import java.util.*;

// 栈
class StackOfferII {
    // 剑指Offer II 025.链表中的两数相加（主站445.）
    // 给定两个 非空链表 l1和 l2 来代表两个非负整数。数字最高位位于链表开始位置。
    // 它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。
    // 可以假设除了数字 0 之外，这两个数字都不会以零开头。

    // 方法一：栈-时间复杂度：O(max(m,n))，空间复杂度：O(m+n)
    // 本题的主要难点在于链表中数位的顺序与我们做加法的顺序是相反的，为了逆序处理所有数位，
    // 我们可以使用栈：把所有数字压入栈中，再依次取出相加。计算过程中需要注意进位的情况。
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        Deque<Integer> stack1 = new LinkedList<Integer>();
        Deque<Integer> stack2 = new LinkedList<Integer>();

        // 两链表分别入栈
        while (l1 != null) {
            stack1.push(l1.val);
            l1 = l1.next;
        }
        while (l2 != null) {
            stack2.push(l2.val);
            l2 = l2.next;
        }

        int carry = 0;// 进位
        ListNode ans = null;
        while (!stack1.isEmpty() || !stack2.isEmpty() || carry != 0) {// 两栈都为空，且无进位时退出循环
            int a = stack1.isEmpty() ? 0 : stack1.pop();
            int b = stack2.isEmpty() ? 0 : stack2.pop();
            int cur = a + b + carry;
            carry = cur / 10;
            cur %= 10;
            ListNode curNode = new ListNode(cur);
            curNode.next = ans;
            ans = curNode;
        }
        return ans;
    }

    // 方法二：（自己写的）在较短链表前填充值为0的节点-时间复杂度：O(max(m,n))，空间复杂度：O(abs(m-n))
    int n1 = 0, n2 = 0;
    int adder = 0;

    public ListNode addTwoNumbers2(ListNode l1, ListNode l2) {
        // 计算得到两个链表长度
        ListNode node1 = l1, node2 = l2;
        while (node1 != null) {
            node1 = node1.next;
            n1++;
        }
        while (node2 != null) {
            node2 = node2.next;
            n2++;
        }

        return n1 >= n2 ? addTwoLinkedList(l1, l2) : addTwoLinkedList(l2, l1);
    }

    // l1 更长
    private ListNode addTwoLinkedList(ListNode l1, ListNode l2) {
        ListNode res = null;
        // 在较短链表前填充值为0的节点
        if (n1 != n2) {
            ListNode dummyHead = new ListNode(0);
            int dummyNum = Math.abs(n1 - n2) - 1;
            ListNode node = dummyHead;
            while (dummyNum != 0) {
                node.next = new ListNode(0);
                node = node.next;
                dummyNum--;
            }
            node.next = l2;
            res = dfs(l1, dummyHead);
        } else
            res = dfs(l1, l2);

        // 若有进位再补个头节点
        if (adder == 1) {
            ListNode newHead = new ListNode(1);
            newHead.next = res;
            return newHead;
        } else
            return res;
    }

    private ListNode dfs(ListNode l1, ListNode l2) {
        if (l1 == null && l2 == null)
            return null;

        ListNode next = dfs(l1.next, l2.next);
        ListNode curr = new ListNode((l1.val + l2.val + adder) % 10);
        adder = (l1.val + l2.val + adder) / 10;
        curr.next = next;
        return curr;
    }

    // 剑指Offer II 026.重排链表（主站143.）
    // 给定一个单链表 L 的头节点 head ，单链表 L 表示为：
    //  L0 → L1 → … → Ln-1 → Ln 
    // 请将其重新排列后变为：
    // L0 → Ln → L1 → Ln-1 → L2 → Ln-2 → …
    // 不能只是单纯的改变节点内部的值，而是需要实际的进行节点交换。

    // 方法一：线性表或双端队列（自己写的）-时间复杂度：O(N)，空间复杂度：O(N)
    public void reorderList(ListNode head) {
        if (head == null)
            return;

        Deque<ListNode> dq = new LinkedList<>();
        ListNode node = head;
        while (node != null) {
            dq.offerLast(node);
            node = node.next;
        }

        ListNode dummyHead = new ListNode(-1);
        ListNode right = dummyHead;
        ListNode tail = null;
        while (!dq.isEmpty()) {
            ListNode left = dq.pollFirst();// 队头出队
            right.next = left;
            tail = left;
            if (!dq.isEmpty()) {
                right = dq.pollLast();// 队尾出队
                left.next = right;
                tail = right;
            }
        }
        tail.next = null;
    }

    // 方法二：寻找链表中点 + 反转后半链表 + 合并链表（两链表节点依次）-时间复杂度：O(N)，空间复杂度：O(1)
    public void reorderList2(ListNode head) {
        if (head == null)
            return;

        ListNode mid = middleNode(head);
        // 链表l1 l2的长度关系：l2 = [l1, l1+1]
        ListNode l1 = head;
        ListNode l2 = mid.next;
        mid.next = null;// 防止两个链表尾指向同一个节点
        l2 = reverseList(l2);
        mergeList(l1, l2);
    }

    // 快慢指针找链表中点
    public ListNode middleNode(ListNode head) {
        ListNode slow = head;
        ListNode fast = head;
        // 精髓之一：这里的判断条件不是常规的fast!=null&&fast.next!=null，为的是找到真正的中心节点的前驱节点
        // 将其指向null，防止两个链表尾指向同一个节点
        while (fast.next != null && fast.next.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        return slow;
    }

    // 反转链表
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {
            ListNode nextTemp = curr.next;
            curr.next = prev;
            prev = curr;
            curr = nextTemp;
        }
        return prev;
    }

    // 合并链表
    public void mergeList(ListNode l1, ListNode l2) {
        ListNode l1_tmp;
        ListNode l2_tmp;
        while (l1 != null && l2 != null) {// 只要有1个为null就退出循环
            l1_tmp = l1.next;
            l2_tmp = l2.next;

            l1.next = l2;
            l1 = l1_tmp;

            l2.next = l1;
            l2 = l2_tmp;
        }
    }

    // 方法二：（自己写的）寻找链表中点 + 反转后半链表 + 合并链表（两链表节点依次）-时间复杂度：O(N)，空间复杂度：O(1)
    public void reorderList22(ListNode head) {
        // 1.寻找链表中点
        ListNode slow = head, fast = head;
        // 这里slow找到的是真正的中点，此时不知道其前驱节点，反转后两链表最后指向同一个节点
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        ListNode firstHead = head;
        ListNode secondHead = slow;

        // 2.反转后半链表
        ListNode prev = null;
        ListNode curr = secondHead;
        while (curr != null) {
            ListNode next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        secondHead = prev;

        // 3.合并链表
        ListNode dummyHead = new ListNode(-1);
        // 精髓之一：
        // 奇数个节点时，firstHead == secondHead时，firstHead一定已经遍历完了，此时secondHead == 中间那个公共节点，
        // 偶数个节点时，firstHead遍历完 == 初始secondHead，而secondHead == null 也会退出
        while (firstHead != secondHead && secondHead != null) {
            dummyHead.next = firstHead;
            firstHead = firstHead.next;
            dummyHead = dummyHead.next;

            dummyHead.next = secondHead;
            secondHead = secondHead.next;
            dummyHead = dummyHead.next;
        }
        if (secondHead != null)
            dummyHead.next = secondHead;
    }

    // 剑指Offer II 027.回文链表（主站234.hot100有）
    // 给定一个链表的 头节点 head ，请判断其是否为回文链表。
    // 如果一个链表是回文，那么链表节点序列从前往后看和从后往前看是相同的。
    // 进阶：能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题？

    // 方法一：将值复制到数组中后用双指针法/双端队列
    // 方法二：递归
    // 方法三：寻找链表中点 + 反转链表
    public boolean isPalindrome(ListNode head) {
        if (head == null)
            return true;

        // 找到前半部分链表的尾节点并反转后半部分链表
        ListNode firstHalfEnd = endOfFirstHalf(head);
        ListNode secondHalfStart = reverseList(firstHalfEnd.next);

        // 判断是否回文
        ListNode p1 = head;
        ListNode p2 = secondHalfStart;

        while (p2 != null) {// p2==NULL时，p1==firstHalfEnd->next，分情况讨论下，链表奇偶数个节点就明朗了
            if (p1.val != p2.val)
                return false;

            p1 = p1.next;
            p2 = p2.next;
        }

        // 还原链表并返回结果
        firstHalfEnd.next = reverseList(secondHalfStart);
        return true;
    }

    // 快慢指针找前半部分链表的末尾节点
    private ListNode endOfFirstHalf(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while (fast.next != null && fast.next.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        return slow;
    }

    // private ListNode reverseList(ListNode head) {
    // ListNode prev = null;
    // ListNode curr = head;
    // while (curr != null) {
    // ListNode nextTemp = curr.next;
    // curr.next = prev;
    // prev = curr;
    // curr = nextTemp;
    // }
    // return prev;
    // }

    // 剑指Offer II 036.后缀表达式（主站150.）
    // 根据 逆波兰表示法，求该后缀表达式的计算结果。
    // 有效的算符包括 +、-、*、/ 。每个运算对象可以是整数，也可以是另一个逆波兰表达式。
    // 说明：
    // 整数除法只保留整数部分。
    // 给定逆波兰表达式总是有效的。换句话说，表达式总会得出有效数值且不存在除数为 0 的情况。

    // 逆波兰表达式：
    // 逆波兰表达式是一种后缀表达式，所谓后缀就是指算符写在后面。
    // 平常使用的算式则是一种中缀表达式，如 ( 1 + 2 ) * ( 3 + 4 ) 。
    // 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。
    // 逆波兰表达式主要有以下两个优点：
    // 去掉括号后表达式无歧义，上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
    // 适合用栈操作运算：遇到数字则入栈；遇到算符则取出栈顶两个数字进行计算，并将结果压入栈中。

    // 方法一：栈-时空复杂度：O(n)
    // 逆波兰表达式严格遵循「从左到右」的运算。计算逆波兰表达式的值时，使用一个栈存储操作数，从左到右遍历逆波兰表达式，进行如下操作：
    // 如果遇到操作数，则将操作数入栈；
    // 如果遇到运算符，则将两个操作数出栈，其中先出栈的是右操作数，后出栈的是左操作数，
    // 使用运算符对两个操作数进行运算，将运算得到的新操作数入栈。
    // 整个逆波兰表达式遍历完毕之后，栈内只有一个元素，该元素即为逆波兰表达式的值。
    public int evalRPN(String[] tokens) {
        Deque<Integer> stack = new LinkedList<Integer>();
        int n = tokens.length;
        for (int i = 0; i < n; i++) {
            String token = tokens[i];
            if (isNumber(token))
                stack.push(Integer.parseInt(token));
            else {
                int num2 = stack.pop();// 右操作数
                int num1 = stack.pop();// 左操作数
                switch (token) {
                    case "+":
                        stack.push(num1 + num2);
                        break;
                    case "-":
                        stack.push(num1 - num2);
                        break;
                    case "*":
                        stack.push(num1 * num2);
                        break;
                    case "/":
                        stack.push(num1 / num2);
                        break;
                    default:
                }
            }
        }
        return stack.pop();
    }

    public boolean isNumber(String token) {
        return !("+".equals(token) || "-".equals(token) || "*".equals(token) || "/".equals(token));
    }

    // 剑指Offer II 037.小行星碰撞（主站735.）
    // 给定一个整数数组 asteroids，表示在同一行的小行星。
    // 对于数组中的每一个元素，其绝对值表示小行星的大小，正负表示小行星的移动方向（正表示向右移动，负表示向左移动）。
    // 每一颗小行星以相同的速度移动 找出碰撞后剩下的所有小行星。
    // 碰撞规则：两个行星相互碰撞，较小的行星会爆炸。如果两颗行星大小相同，则两颗行星都会爆炸。两颗移动方向相同的行星，永远不会发生碰撞。

    // 方法一：辅助栈-时空复杂度：O(n)
    public int[] asteroidCollision(int[] asteroids) {
        Stack<Integer> stack = new Stack<>();
        for (int ast : asteroids) {
            while (!stack.isEmpty() && ast < 0 && stack.peek() > 0 && stack.peek() < -ast) // 栈顶行星爆炸
                stack.pop();

            if (!stack.isEmpty() && ast < 0 && stack.peek() == -ast) // 栈顶、当前行星一起爆炸
                stack.pop();
            else if (!stack.isEmpty() && ast < 0 && stack.peek() > -ast) {// 当前行星爆炸

            } else // 1. 当前为负行星但栈为空 2. 当前为负行星栈顶行星也为负 3. 当前为正行星
                stack.push(ast);
        }

        int[] ans = new int[stack.size()];
        for (int t = ans.length - 1; t >= 0; --t)
            ans[t] = stack.pop();

        return ans;
    }

    // 方法一：（自己写的）辅助栈-时空复杂度：O(n)
    // 个人觉得这个稍长，但是把判断条件逐层拆开，逻辑更清楚
    public int[] asteroidCollision11(int[] asteroids) {
        Deque<Integer> stk = new LinkedList<>();
        for (int asteroid : asteroids) {
            if (asteroid > 0) // 1.当前行星向右，直接入栈
                stk.push(asteroid);
            else {// 2.当前行星向左，需要判断碰撞
                while (!stk.isEmpty()) {
                    int top = stk.peek();
                    if (top < 0) // 2.1.栈顶行星向左，不会产生碰撞
                        break;
                    else {// 2.2.栈顶行星向右，需要根据质量判断碰撞结果
                          // 将当前行星质量置为最小值，表示当前行星已经炸了
                        if (top == -asteroid) {// 2.2.1.质量相等，一起炸
                            stk.pop();
                            asteroid = Integer.MIN_VALUE;
                            break;
                        } else if (top > -asteroid) {// 2.2.2.当前行星炸
                            asteroid = Integer.MIN_VALUE;
                            break;
                        } else // 2.2.3.栈顶行星炸
                            stk.pop();
                    }
                }
                if (asteroid != Integer.MIN_VALUE)// 判断碰撞后，当前行星是否还存在
                    stk.push(asteroid);
            }
        }
        int[] res = new int[stk.size()];
        for (int i = 0; i < res.length; i++)
            res[i] = stk.pollLast();// Deque模拟栈时是队头进出，所以按照相对顺序得从队尾开始遍历

        return res;
    }

    // 剑指Offer II 038.每日温度（主站739.hot100有）
    // 请根据每日 气温 列表 temperatures ，重新生成一个列表，
    // 要求其对应位置的输出为：要想观测到更高的气温，至少需要等待的天数。如果气温在这之后都不会升高，请在该位置用 0 来代替。

    // 方法一：单调栈-时间复杂度：O(n)，空间复杂度：O(n)
    public int[] dailyTemperatures(int[] temperatures) {
        int length = temperatures.length;
        int[] ans = new int[length];// 初始值为0
        Deque<Integer> stack = new LinkedList<Integer>();// 存储下标的单调栈
        for (int i = 0; i < length; i++) {
            int temperature = temperatures[i];
            // 栈非空，且入栈温度高于栈顶温度，此时可根据下标循环更新ans数组
            while (!stack.isEmpty() && temperature > temperatures[stack.peek()]) {
                int prevIndex = stack.pop();
                ans[prevIndex] = i - prevIndex;
            }
            stack.push(i);// 栈为空，或入栈温度低于栈顶温度
        }
        return ans;
    }

    // 方法二：暴力 反向遍历/正向遍历

    // 剑指Offer II 039.直方图最大的矩形（主站84.hot100有）
    // 给定非负整数数组 heights ，数组中的数字用来表示柱状图中各个柱子的高度。每个柱子彼此相邻，且宽度为 1 。
    // 求在该柱状图中，能够勾勒出来的矩形的最大面积。

    // 方法一：单调栈 两次遍历
    // 从左至右遍历（单调递增），确定左边界；从右至左遍历（单调递增），确定右边界

    // 方法二：单调栈 一次遍历-时空复杂度：O(n)
    // 从左至右遍历（单调递增），入栈时确定左边界，出栈时确定右边界
    public int largestRectangleArea(int[] heights) {
        int n = heights.length;

        // 开区间
        int[] left = new int[n];
        int[] right = new int[n];
        Arrays.fill(right, n);// 初始为n 1.最后一个元素 2.右边全比他高（遍历完都没有出栈）

        Deque<Integer> mono_stack = new ArrayDeque<Integer>();// 存放下标
        for (int i = 0; i < n; ++i) {
            while (!mono_stack.isEmpty() && heights[mono_stack.peek()] >= heights[i]) {
                right[mono_stack.peek()] = i;// 元素在出栈时确定了其右边界
                mono_stack.pop();
            }
            left[i] = (mono_stack.isEmpty() ? -1 : mono_stack.peek());
            mono_stack.push(i);// 元素在入栈时，确定了它的左边界
        }

        int ans = 0;
        for (int i = 0; i < n; ++i)
            ans = Math.max(ans, (right[i] - left[i] - 1) * heights[i]);

        return ans;
    }

    // 剑指Offer II 040.矩阵中最大的矩形（主站85.hot100有）
    // 给定一个由 0 和 1 组成的矩阵 matrix ，找出只包含 1 的最大矩形，并返回其面积。
    // 注意：此题 matrix 输入格式为一维 01 字符串数组。

    // 方法一：单调栈-时间复杂度：O(mn)，空间复杂度：O(mn)
    // 计算每个柱状图中的最大面积，并找到全局最大值。
    // 我们可以使用39.中的单调栈的做法，将其应用在我们生成的柱状图中。
    public int maximalRectangle(String[] matrix) {
        int m = matrix.length;
        if (m == 0)
            return 0;

        int n = matrix[0].length();
        int[][] height = new int[m][n];

        for (int i = 0; i < m; i++) // 统计第i行高度
            for (int j = 0; j < n; j++)
                if (matrix[i].charAt(j) == '1')// 为0的不处理，高度一定是0
                    if (i == 0)// 防越界
                        height[i][j] = 1;
                    else
                        height[i][j] = height[i - 1][j] + 1;

        int ret = 0;
        for (int i = 0; i < m; i++) // 对于每一列，使用基于柱状图的方法
            ret = Math.max(ret, largestRectangleArea(height[i]));

        return ret;
    }

    // 方法一：（自己写的）单调栈-时间复杂度：O(mn)，空间复杂度：O(n)
    public int maximalRectangle11(String[] matrix) {
        int res = 0;
        int m = matrix.length;
        if (m == 0)
            return res;

        int n = matrix[0].length();
        int[] heights = new int[n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (matrix[i].charAt(j) == '0')
                    heights[j] = 0;
                else
                    heights[j]++;
            }
            res = Math.max(res, largestRectangleArea(heights));
        }
        return res;
    }

    // 剑指Offer II 052.展平二叉搜索树（主站897.）
    // 给你一棵二叉搜索树，请 按中序遍历 将其重新排列为一棵递增顺序搜索树，使树中最左边的节点成为树的根节点，并且每个节点没有左子节点，只有一个右子节点。

    // 方法一：先栈记录中序遍历，再改变树的结构
    public TreeNode increasingBST(TreeNode root) {
        Deque<TreeNode> stk = new LinkedList<>();
        dfs(root, stk);
        TreeNode res = stk.peekFirst();
        while (!stk.isEmpty()) {
            TreeNode node = stk.pollFirst();
            node.left = null;
            if (!stk.isEmpty())
                node.right = stk.peekFirst(); // 末尾节点的右指针一定为空
        }

        return res;
    }

    void dfs(TreeNode node, Deque<TreeNode> stk) {
        if (node == null)
            return;
        dfs(node.left, stk);
        stk.offerLast(node);
        dfs(node.right, stk);
    }

    // 方法二：中序遍历的同时改变树的结构
    private TreeNode prevNode;

    public TreeNode increasingBST2(TreeNode root) {
        TreeNode dummyNode = new TreeNode(-1);
        prevNode = dummyNode;
        inorder(root);
        return dummyNode.right;
    }

    public void inorder(TreeNode node) {
        if (node == null)
            return;

        inorder(node.left);

        // 在中序遍历的过程中修改节点指向
        prevNode.right = node;
        node.left = null;
        prevNode = node;

        inorder(node.right);
    }

    // 剑指Offer II ·二叉搜索树迭代器（主站173.）
    // 实现一个二叉搜索树迭代器类BSTIterator ，表示一个按中序遍历二叉搜索树（BST）的迭代器：
    // BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。
    // BST 的根节点 root 会作为构造函数的一部分给出。
    // 指针应初始化为一个不存在于 BST 中的数字，且该数字小于 BST 中的任何元素。
    // boolean hasNext() 如果向指针右侧遍历存在数字，则返回 true ；否则返回 false 。
    // int next()将指针向右移动，然后返回指针处的数字。
    // 注意，指针初始化为一个不存在于 BST 中的数字，所以对 next() 的首次调用将返回 BST 中的最小元素。
    // 可以假设 next() 调用总是有效的，也就是说，当调用 next() 时，BST 的中序遍历中至少存在一个下一个数字。
    // 进阶：
    // 你可以设计一个满足下述条件的解决方案吗？next() 和 hasNext() 操作均摊时间复杂度为 O(1) ，并使用 O(h) 内存。
    // 其中 h 是树的高度。

    // 方法一：迭代-时间复杂度：O(1)，空间复杂度：O(h)
    // 除了递归的方法外，我们还可以利用栈这一数据结构，通过迭代的方式对二叉树做中序遍历。
    // 此时，我们无需预先计算出中序遍历的全部结果，只需要实时维护当前栈的情况即可。
    class BSTIterator {
        private TreeNode cur;
        private Deque<TreeNode> stack;

        public BSTIterator(TreeNode root) {
            cur = root;
            stack = new LinkedList<TreeNode>();
        }

        public int next() {
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }
            cur = stack.pop();
            int ret = cur.val;
            cur = cur.right;
            return ret;
        }

        public boolean hasNext() {
            return cur != null || !stack.isEmpty();
        }
    }

    // 方法一：迭代（自己写的，稍有不同，少使用一个成员变量）-时间复杂度：O(1)，空间复杂度：O(h)
    class BSTIterator11 {
        Deque<TreeNode> stk = new ArrayDeque<>();

        public BSTIterator11(TreeNode root) {
            while (root != null) {
                stk.push(root);
                root = root.left;
            }
        }

        public int next() {
            while (!stk.isEmpty()) {
                TreeNode root = stk.pop();// 出栈元素即为下一个元素
                if (root.right != null) {// 按照中序遍历的顺序，将出栈元素的右子树中所有左孩子入栈
                    TreeNode node = root.right;
                    while (node != null) {
                        stk.push(node);
                        node = node.left;
                    }
                }
                return root.val;
            }
            return -1;// 根据题意，其实不会执行到这里
        }

        public boolean hasNext() {
            return !stk.isEmpty();
        }
    }

    // 方法二：扁平化 栈存储
    // 时间复杂度：初始化需要 O(n) 的时间。随后每次调用只需要 O(1) 的时间。
    // 空间复杂度：O(n)
    // 我们可以直接对二叉搜索树做一次完全的递归遍历，获取中序遍历的全部结果并保存在数组中。随后，我们利用得到的数组本身来实现迭代器。
    class BSTIterator2 {
        private int idx;
        private List<Integer> arr;

        public BSTIterator2(TreeNode root) {
            idx = 0;
            arr = new ArrayList<Integer>();
            inorderTraversal(root, arr);
        }

        public int next() {
            return arr.get(idx++);
        }

        public boolean hasNext() {
            return idx < arr.size();
        }

        private void inorderTraversal(TreeNode root, List<Integer> arr) {
            if (root == null)
                return;

            inorderTraversal(root.left, arr);
            arr.add(root.val);
            inorderTraversal(root.right, arr);
        }
    }

}

// 位运算
class BitOperationOfferII {
    // 剑指Offer II 001.整数除法（主站29.）
    // 给定两个整数 a 和 b ，求它们的除法的商 a/b ，要求不得使用乘号 '*'、除号 '/' 以及求余符号 '%' 。
    // 注意：
    // 整数除法的结果应当截去（truncate）其小数部分，例如：truncate(8.345) = 8 以及 truncate(-2.7335) = -2
    // 假设我们的环境只能存储 32 位有符号整数，其数值范围是 [−231, 231−1]。本题中，如果除法结果溢出，则返回 231 − 1
    // 不使用二分查找，直接逐个减去除数会超时

    // 方法一：二分查找-时间复杂度：O(log2C)，空间复杂度：O(1)
    // 记被除数为 X，除数为 Y，并且 X 和 Y 都是负数。我们需要找出 X/Y 的结果 Z。Z 一定是正数或 0。
    // Z × Y ≥ X > (Z+1) × Y
    // 即找到第一个不满足条件的数，二分查找呼之欲出
    public int divide(int a, int b) {
        // 32 位有符号整数，其数值范围是 [−231, 231−1]

        // 考虑被除数为最小值的情况
        if (a == Integer.MIN_VALUE) {
            if (b == 1)
                return Integer.MIN_VALUE;
            if (b == -1)
                return Integer.MAX_VALUE;
        }

        // 考虑除数为最小值的情况
        if (b == Integer.MIN_VALUE)
            return a == Integer.MIN_VALUE ? 1 : 0;

        // 考虑被除数为 0 的情况
        if (a == 0)
            return 0;

        // 一般情况，使用二分查找
        // 将所有的正数取相反数，这样就只需要考虑一种情况
        boolean rev = false;
        if (a > 0) {
            a = -a;
            rev = !rev;
        }
        if (b > 0) {
            b = -b;
            rev = !rev;
        }

        // Z × Y ≥ X > (Z+1) × Y （都为负数）
        int left = 1, right = Integer.MAX_VALUE, ans = 0;
        while (left <= right) {
            // 注意溢出，并且不能使用除法
            int mid = left + ((right - left) >> 1);
            boolean check = quickAdd(b, mid, a);
            if (check) {
                ans = mid;
                // 注意溢出
                if (mid == Integer.MAX_VALUE)
                    break;
                left = mid + 1;
            } else
                right = mid - 1;

        }

        return rev ? -ans : ans;
    }

    // 快速乘 只需要在「快速幂」算法的基础上，将乘法运算改成加法运算即可。
    // x 和 y 是负数，z 是正数
    // 需要判断 z * y >= x 是否成立
    public boolean quickAdd(int y, int z, int x) {
        int result = 0, add = y;
        while (z != 0) {
            if ((z & 1) != 0) {
                // 需要保证 result + add >= x
                if (result < x - add)// 一定是作差比较，防越界
                    return false;
                result += add;
            }
            if (z != 1) {
                // 需要保证 add + add >= x
                if (add < x - add)
                    return false;
                add += add;
            }
            // 不能使用除法
            z >>= 1;
        }
        return true;
    }

    // 方法一：（自己写的）二分查找-时间复杂度：O(log2C)，空间复杂度：O(1)
    public int divide11(int a, int b) {
        if (a == Integer.MIN_VALUE) {
            if (b == 1)
                return Integer.MIN_VALUE;
            if (b == -1)// 开头就把唯一的结果越界处理了
                return Integer.MAX_VALUE;
        }

        // 偷懒，方便处理数据溢出问题
        long sign = 1L;
        long A = (long) a;
        long B = (long) b;

        // 转为正数方便后续处理
        if (A < 0) {
            sign = -sign;
            A = -A;
        }
        if (B < 0) {
            sign = -sign;
            B = -B;
        }

        // 处理一些可以直接返回结果的情况
        if (A == 0L)
            return 0;
        if (A == B)
            return (int) sign;
        if (A < B)
            return 0;
        if (B == 1L)
            return sign == 1L ? (int) A : (int) -A;

        // 设置结果的左右边界
        long left = 0;
        long right = A;
        while (left <= right) {
            long mid = left + ((right - left) >> 1);
            long product = muiltipy(B, mid, A);
            if (product < A) {
                left = mid + 1L;
            } else {
                if (product == A)
                    return sign == 1L ? (int) mid : (int) -mid;
                right = mid - 1L;
            }
        }
        return sign == 1L ? (int) right : (int) -right;

    }

    private long muiltipy(long x, long y, long max) {
        long ans = 0;
        long base = x;
        while (y != 0) {
            if ((y & 1) == 1)
                ans += base;
            if (ans > max)// 精髓之一：防止越界，及无谓的运算
                return max + 1;
            base = base + base;
            y = y >> 1;
        }
        return ans;
    }

    // 方法二：类二分查找-时间复杂度：O(logC)，空间复杂度：O(logC)
    // 记被除数为 X，除数为 Y，并且 X 和 Y 都是负数。我们需要找出 X/Y 的结果 Z。Z 一定是正数或 0。
    // Z × Y ≥ X > (Z+1) × Y
    // 首先不断地将 Y 乘以 2（通过加法运算实现），并将这些结果放入数组中，其中数组的第 i 项就等于 Y×2^i 。
    // 这一过程直到 Y 的两倍严格小于 X 为止。
    // 我们对数组进行逆序遍历。当遍历到第 i 项时，如果其大于等于 X，我们就将答案增加 2^i，并且将 X 中减去这一项的值。

    public int divide2(int dividend, int divisor) {
        // 考虑被除数为最小值的情况
        if (dividend == Integer.MIN_VALUE) {
            if (divisor == 1)
                return Integer.MIN_VALUE;
            if (divisor == -1)
                return Integer.MAX_VALUE;
        }
        // 考虑除数为最小值的情况
        if (divisor == Integer.MIN_VALUE)
            return dividend == Integer.MIN_VALUE ? 1 : 0;

        // 考虑被除数为 0 的情况
        if (dividend == 0)
            return 0;

        // 一般情况，使用类二分查找
        // 将所有的正数取相反数，这样就只需要考虑一种情况
        boolean rev = false;
        if (dividend > 0) {
            dividend = -dividend;
            rev = !rev;
        }
        if (divisor > 0) {
            divisor = -divisor;
            rev = !rev;
        }

        List<Integer> candidates = new ArrayList<Integer>();// 存储 Y×2^i
        candidates.add(divisor);
        int index = 0;
        // 注意溢出
        while (candidates.get(index) >= dividend - candidates.get(index)) {// 直到 Y 的两倍严格小于 X 为止，且防溢出
            candidates.add(candidates.get(index) + candidates.get(index));
            ++index;
        }
        int ans = 0;
        for (int i = candidates.size() - 1; i >= 0; --i)
            if (candidates.get(i) >= dividend) {
                ans += 1 << i;// += 2^i
                dividend -= candidates.get(i);
            }

        return rev ? -ans : ans;
    }

    // 剑指Offer II 002.二进制加法（主站67.）
    // 给定两个 01 字符串 a 和 b ，请计算它们的和，并以二进制字符串的形式输出。
    // 输入为 非空 字符串且只包含数字 1 和 0。

    // 方法一：模拟 + 位运算-时间复杂度：O(max(m, n))，空间复杂度：O(1)
    public String addBinary(String a, String b) {
        StringBuffer ans = new StringBuffer();
        int n = Math.max(a.length(), b.length());
        int carry = 0;
        for (int i = 0; i < n; ++i) {// 从低位开始遍历作和
            if (i < a.length())
                carry += a.charAt(a.length() - 1 - i) - '0';
            if (i < b.length())
                carry += b.charAt(b.length() - 1 - i) - '0';
            ans.append((char) (carry % 2 + '0'));
            carry /= 2; // 3, 2 → 1 。 1 → 0
        }

        if (carry > 0)// 最后还剩进位
            ans.append('1');

        ans.reverse();
        return ans.toString();
    }

    // 方法一：（自己写的）模拟 + 位运算-时间复杂度：O(max(m, n))，空间复杂度：O(1)
    public String addBinary11(String a, String b) {
        int m = a.length();
        int n = b.length();
        StringBuilder sb = new StringBuilder();
        int adder = 0;
        while (m != 0 || n != 0) {
            int curr = 0;
            if (m != 0)
                curr += a.charAt(m-- - 1) - '0';
            if (n != 0)
                curr += b.charAt(n-- - 1) - '0';

            curr += adder;
            adder = (curr) / 2;
            sb.append(curr % 2);
        }

        if (adder == 1)
            sb.append(1);

        return sb.reverse().toString();
    }

    // 剑指Offer II 003.前n个数字二进制中 1 的个数（主站338.hot100有）
    // 进阶:
    // 给出时间复杂度为 O(n*sizeof(integer)) 的解答非常容易。但你可以在线性时间 O(n) 内用一趟扫描做到吗？
    // 要求算法的空间复杂度为 O(n) 。
    // 你能进一步完善解法吗？要求在C++或任何其他语言中不使用任何内置函数（如 C++ 中的 __builtin_popcount ）来执行此操作。

    // 方法一：java内置函数Integer.bitCount-时间复杂度O(nlogn)
    // 方法二：Brian Kernighan 算法-对 0 到 n 的每个整数直接计算「一比特数」-时间复杂度O(nlogn)
    // x = x & (x − 1)
    // 方法三：循环检查-时间复杂度O(nlogn)
    // 方法四：动态规划——最高有效位（后面两个更好理解）-时间复杂度O(n)
    // 方法五：动态规划——最低有效位-时间复杂度O(n)
    public int[] countBits(int n) {
        int[] res = new int[n + 1];
        for (int i = 1; i <= n; i++)
            res[i] = res[i >> 1] + (i & 1);

        return res;
    }

    // 剑指Offer II 004.只出现一次的数字（主站137.）
    // 给你一个整数数组 nums ，除某个元素仅出现 一次 外，其余每个元素都恰出现 三次 。请你找出并返回那个只出现了一次的元素。

    // 方法一：哈希表-时间复杂度：O(n)，空间复杂度：O(n)
    // 思路与算法
    // 我们可以使用哈希映射统计数组中每个元素的出现次数。对于哈希映射中的每个键值对，键表示一个元素，值表示其出现的次数。
    // 在统计完成后，我们遍历哈希映射即可找出只出现一次的元素。

    // 方法二：（自己写的）依次确定每一个二进制位-时间复杂度：O(nlogC)，空间复杂度：O(1)
    // 答案的第 i 个二进制位就是数组中所有元素的第 i 个二进制位之和除以 3 的余数。
    public int singleNumber2(int[] nums) {
        int pos = 1;
        int res = 0;
        for (int i = 0; i < 32; i++) {
            int count = 0;
            for (int num : nums)
                if ((num & pos) == pos)
                    count++;

            if (count % 3 == 1)
                res = res | pos;
            pos = pos << 1;
        }
        return res;

    }

    // 方法三：数字电路设计-时间复杂度：O(n)，空间复杂度：O(1)
    // 在方法二中，我们是依次处理每一个二进制位的，那么时间复杂度中就引入了 O(logC) 这一项。
    // 既然我们在对两个整数进行普通的二元运算时，都是将它们看成整体进行处理的，那么我们是否能以普通的二元运算为基础，同时处理所有的二进制位？
    // 黑盒中存储了两个整数 a 和 b，且会有三种情况：
    // a 的第 i 位为 0 且 b 的第 i 位为 0，表示 0；
    // a 的第 i 位为 0 且 b 的第 i 位为 1，表示 1；
    // a 的第 i 位为 1 且 b 的第 i 位为 0，表示 2。
    public int singleNumber3(int[] nums) {
        int a = 0, b = 0;
        for (int num : nums) {
            int aNext = (~a & b & num) | (a & ~b & ~num);
            int bNext = ~a & (b ^ num);
            a = aNext;
            b = bNext;
        }
        return b;// 直接返回b就是最终答案，b 的第 i 位为 0，表示 0（最终状态下，不可能表示2）；b 的第 i 位为 1，表示 1；
    }

    // 方法四：数字电路设计优化-时间复杂度：O(n)，空间复杂度：O(1)
    // 先计算b再计算a，a的真值表使用计算后的b会变简单
    public int singleNumber4(int[] nums) {
        int a = 0, b = 0;
        for (int num : nums) {
            b = ~a & (b ^ num);
            a = ~b & (a ^ num);
        }
        return b;
    }

    // 剑指Offer II 005.单词长度的最大乘积（主站318.）
    // 给定一个字符串数组 words，请计算当两个字符串 words[i] 和 words[j] 不包含相同字符时，它们长度的乘积的最大值。
    // 假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串，返回 0。
    // 提示：
    // 2 <= words.length <= 1000
    // 1 <= words[i].length <= 1000
    // words[i] 仅包含小写字母
    // 为了得到最大单词长度乘积，朴素的做法是，遍历字符串数组 words 中的每一对单词，判断这一对单词是否有公共字母，
    // 如果没有公共字母，则用这一对单词的长度乘积更新最大单词长度乘积。
    // 该时间复杂度高于 O(n^2)
    // 难点在于快速判断一对单词是否有公共字母
    // 如果可以将判断两个单词是否有公共字母的时间复杂度降低到 O(1)，则可以将总时间复杂度降低到 O(n^2)

    // 方法一：位运算-时间复杂度：O(L + n^2)，空间复杂度：O(n)
    // 可以使用位运算预处理每个单词，通过位运算操作判断两个单词是否有公共字母。
    // 由于单词只包含小写字母，共有 26 个小写字母，因此可以使用位掩码的最低 26 位分别表示每个字母是否在这个单词中出现。26<31
    // 将 a 到 z 分别记为第 0 个字母到第 25 个字母，则位掩码的从低到高的第 i 位是 1 当且仅当第 i 个字母在这个单词中，其中 0≤i≤25。
    public int maxProduct(String[] words) {
        int length = words.length;
        int[] masks = new int[length];// 用数组 masks 记录每个单词的位掩码表示
        for (int i = 0; i < length; i++) {
            String word = words[i];
            int wordLength = word.length();
            for (int j = 0; j < wordLength; j++)
                masks[i] |= 1 << (word.charAt(j) - 'a');
        }

        int maxProd = 0;
        for (int i = 0; i < length; i++)
            for (int j = i + 1; j < length; j++)
                if ((masks[i] & masks[j]) == 0)
                    maxProd = Math.max(maxProd, words[i].length() * words[j].length());

        return maxProd;
    }

    // 方法一：（自己写的）位运算-时间复杂度：O(L + n^2)，空间复杂度：O(n)
    // 小优化一下，两个for循环合在一起，边计算mask，边比较之前的mask
    public int maxProduct2(String[] words) {
        int n = words.length;
        int masks[] = new int[n];
        int res = 0;
        for (int i = 0; i < n; i++) {
            String word = words[i];
            for (char c : word.toCharArray()) {
                int bit = 1 << (c - 'a');
                masks[i] = masks[i] | bit;
            }

            int len1 = word.length();
            for (int j = 0; j < i; j++)
                if ((masks[i] & masks[j]) == 0)
                    res = Math.max(res, len1 * words[j].length());
        }

        return res;
    }

    // 剑指Offer II 067.最大的异或（主站421.）
    // 给定一个整数数组 nums ，返回 nums[i] XOR nums[j] 的最大运算结果，其中 0 ≤ i ≤ j < n 。
    // 提示：
    // 1 <= nums.length <= 2 * 105
    // 0 <= nums[i] <= 231 - 1
    // 进阶：你可以在 O(n) 的时间解决这个问题吗？
    // O(n2)会直接超时

    // 方法一：哈希表-时间复杂度：O(nlogC)，空间复杂度：O(n)
    // 最简单的方法是使用二重循环枚举 i 和 j，但这样做的时间复杂度为 O(n^2)，会超出时间限制。
    // 根据按位异或运算的性质，x = a_i ^ a_j 等价于 a_j = x ^ a_i。
    // 我们可以根据这一变换，设计一种「从高位到低位依次确定 x 二进制表示的每一位」的方法，以此得到 x 的值。
    // 简单来讲，将所有数的前k位加入set，从高位开始判断当前为是否有01，根据之前高位结果，依次判断每一位是否存在01
    // e.g. 从最高位开始，假设x=1，判断是否存在(1, 0)的组合，判断次高位时，假设x=11，判断是否存在(10, 01) (11, 00)的组合

    // 最高位的二进制位编号为 30
    static final int HIGH_BIT = 30;

    public int findMaximumXOR(int[] nums) {
        int x = 0;
        // 实际样例中有大于2^31的数，最大异或值需要31位存储，需要从右移30位，而不是29位开始
        for (int k = HIGH_BIT; k >= 0; --k) {// 确定x的第k位
            Set<Integer> seen = new HashSet<Integer>();
            // 将所有的 pre^k(a_j)前k位 放入哈希表中
            for (int num : nums) // 如果只想保留从最高位开始到第 k 个二进制位为止的部分，只需将其右移 k 位
                seen.add(num >> k);

            // 目前 x 包含从最高位开始到第 k+1 个二进制位为止的部分
            // 我们将 x 的第 k 个二进制位置为 1，即为 x = x*2+1 （假设有ai aj 满足第k位为1）
            int xNext = x * 2 + 1;// 左移一位，最低位置为1
            boolean found = false;

            for (int num : nums) // 枚举 i
                if (seen.contains(xNext ^ (num >> k))) {// 根据所有ai与假设的k计算，看看有没有aj存在
                    found = true;
                    break;
                }

            if (found)
                x = xNext;// e.g. 111
            else // 如果没有找到满足等式的 a_i 和 a_j，那么 x 的第 k 个二进制位只能为 0，即为 x = x*2
                x = xNext - 1; // e.g. 110
        }
        return x;
    }

    // 方法一：（自己写的）哈希表-时间复杂度：O(nlogC)，空间复杂度：O(n)
    public int findMaximumXOR11(int[] nums) {
        int BITS = 30;
        int desired = 1;
        int res = 0;
        for (int i = BITS; i >= 0; i--) {
            Set<Integer> set = new HashSet<>();
            boolean findDesired = false;
            for (int num : nums) {
                int curr = (num >> i);
                if (set.contains(desired ^ curr)) {
                    findDesired = true;
                    break;
                }
                set.add(curr);
            }
            if (findDesired) // 当前位异或可以为1
                res = desired;
            else {// 当前位异或只能为0
                desired = desired - 1;
                res = desired;
            }
            desired = (desired << 1) + 1;// 期待下一位为1
        }
        return res;
    }

    // 方法二：字典树-时间复杂度：O(nlogC)，空间复杂度：O(nlogC)
    // 根据数据选择多少位表示（字典树多少层），以及位运算，非常考验细节
    // 思路与算法
    // 0 <= nums[i] <= 231 - 1
    // 我们也可以将数组中的元素看成长度为 30 的字符串，字符串中只包含 0 和 1。
    // 如果我们将字符串放入字典树中，那么在字典树中查询一个字符串的过程，恰好就是从高位开始确定每一个二进制位的过程。
    // 字典树的具体逻辑以及实现可以参考「208. 实现 Trie（前缀树）的官方题解」

    // 由于字典树中的每个节点最多只有两个子节点，分别表示 0 和 1，因此本题中的字典树是一棵二叉树。
    // 在设计字典树的数据结构时，我们可以令左子节点 left 表示 0，右子节点 right 表示 1。
    // 字典树的建立从高位到低位
    class Trie {
        // 左子树指向表示 0 的子节点
        Trie left = null;
        // 右子树指向表示 1 的子节点
        Trie right = null;
    }

    // 字典树的根节点
    Trie root = new Trie();

    // 最高位的二进制位编号为 30
    // static final int HIGH_BIT = 30;
    public int findMaximumXOR2(int[] nums) {
        int n = nums.length;
        int x = 0;
        for (int i = 1; i < n; ++i) {
            add(nums[i - 1]);// 将 nums[i-1] 放入字典树，此时 nums[0 .. i-1] 都在字典树中
            x = Math.max(x, check(nums[i]));// 将 nums[i] 看作 ai，找出最大的 x 更新答案
        }
        return x;
    }

    /**
     * 将 num 放入字典树
     */
    public void add(int num) {
        Trie cur = root;
        for (int k = HIGH_BIT; k >= 0; --k) {
            int bit = (num >> k) & 1;
            if (bit == 0) {
                if (cur.left == null)// 无分支则创建分支
                    cur.left = new Trie();
                cur = cur.left;
            } else {
                if (cur.right == null)// 无分支则创建分支
                    cur.right = new Trie();
                cur = cur.right;
            }
        }
    }

    /**
     * 找到与num异或值最大的数
     */
    public int check(int num) {
        Trie cur = root;
        int x = 0;
        for (int k = HIGH_BIT; k >= 0; --k) {
            int bit = (num >> k) & 1;
            if (bit == 0) {// a_i 的第 k 个二进制位为 0，应当往表示 1 的子节点 right 走
                if (cur.right != null) {
                    cur = cur.right;
                    x = x * 2 + 1;// 左移，最低位置为1
                } else {
                    cur = cur.left;
                    x = x * 2;// 左移
                }
            } else { // a_i 的第 k 个二进制位为 1，应当往表示 0 的子节点 left 走
                if (cur.left != null) {
                    cur = cur.left;
                    x = x * 2 + 1;// 左移，最低位置为1
                } else {
                    cur = cur.right;
                    x = x * 2;// 左移
                }
            }
        }
        return x;
    }

    // 方法二：（自己写的）字典树-时间复杂度：O(nlogC)，空间复杂度：O(nlogC)
    // 区别在于先将全部数添加到树中，再递归求得最大异或值
    class Trie67 {
        Trie67[] vals = new Trie67[2];
        int val;
    }

    public int findMaximumXOR22(int[] nums) {
        Trie67 root = new Trie67();
        // // 0 <= nums[i] <= 231 - 1，30位数即可表示所有的数
        int HiGH_BIT = 30;

        for (int num : nums) {
            Trie67 curr = root;
            // 最高位右移29即可
            for (int i = HiGH_BIT - 1; i >= 0; i--) {
                int index = (num >> i) & 1;
                if (curr.vals[index] == null)
                    curr.vals[index] = new Trie67();
                curr = curr.vals[index];
            }
            curr.val = num;
        }

        int res = dfs(root, root, HiGH_BIT);
        return res;
    }

    // 递归求得最大异或值
    private int dfs(Trie67 higher, Trie67 lower, int index) {
        if (higher == null || lower == null)// 不存在该组合的值，提前返回
            return 0;

        if (index == 0)// 返回该组合下的异或值
            return higher.val ^ lower.val;

        index--;
        // 优先考虑该位异或为1的组合
        int ans1 = dfs(higher.vals[1], lower.vals[0], index);
        int ans2 = dfs(higher.vals[0], lower.vals[1], index);
        if (ans1 != 0 || ans2 != 0) // 若两种组合非0（有对应组合），则直接返回
            return Math.max(ans1, ans2);

        // 退而求其次，考虑该位异或为0的组合
        int ans3 = dfs(higher.vals[1], lower.vals[1], index);
        int ans4 = dfs(higher.vals[0], lower.vals[0], index);
        return Math.max(ans3, ans4);
    }

    // 剑指Offer II 079.所有子集（主站78.hot100有）
    // 给定一个整数数组 nums ，数组中的元素 互不相同 。返回该数组所有可能的子集（幂集）。
    // 解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

    // 方法一：迭代法实现子集枚举-时间复杂度O(n×2^n)
    // 用 1 表示「在子集中」，0 表示「不在子集中」
    // 可以发现 0/10/1 序列对应的二进制数正好从 0 到 2^n - 1
    // 可以枚举mask∈[0,2^n−1]，mask 的二进制表示是一个 0/1 序列，可以按照这个 0/1 序列在原集合当中取数。
    // 当枚举完所有 2^n-1 个mask，也就能构造出所有的子集
    public List<List<Integer>> subsets(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        int n = nums.length;
        for (int mask = 0; mask < (1 << n); mask++) {
            List<Integer> ans = new ArrayList<>();
            for (int i = 0; i < n; i++)
                if (((mask >> i) & 1) == 1)
                    ans.add(nums[i]);
            res.add(ans);
        }
        return res;
    }

    // 方法二：递归法实现子集枚举（回溯、剪枝）-时间复杂度O(n×2^n)
    // 类似于树的深度优先遍历
    // 0左子树 1右子树
    List<Integer> t = new ArrayList<Integer>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();

    List<List<Integer>> subsets2(int[] nums) {
        dfs(0, nums);
        return ans;
    }

    void dfs(int cur, int[] nums) {
        if (cur == nums.length) {// 此时所有位已枚举完成，（回溯、剪枝）
            ans.add(new ArrayList<Integer>(t));
            return;
        }
        // 选择加入当前位置cur的元素
        t.add(nums[cur]);// 置于末尾
        dfs(cur + 1, nums);

        // 选择不加入当前位置cur的元素
        t.remove(t.size() - 1);// 移除末尾元素
        dfs(cur + 1, nums);
    }
}

// 树
class TreeOfferII {
    // 剑指Offer II 043.往完全二叉树添加节点（主站919.）
    // 完全二叉树是每一层（除最后一层外）都是完全填充（即，节点数达到最大）的，并且所有的节点都尽可能地集中在左侧。
    // 设计一个用完全二叉树初始化的数据结构 CBTInserter，它支持以下几种操作：
    // CBTInserter(TreeNode root) 使用头节点为 root 的给定树初始化该数据结构；
    // CBTInserter.insert(int v)  向树中插入一个新节点，节点类型为 TreeNode，值为 v 。
    // 使树保持完全二叉树的状态，并返回插入的新节点的父节点的值；
    // CBTInserter.get_root() 将返回树的头节点。

    // 注意：不是完全二叉搜索树

    // 方法一：双端队列：时空复杂度：O(n)
    // 在每个插入步骤中，我们希望插入到一个编号最小的节点（孩子未满的节点，该节点有 0-1 个孩子）。
    // 通过维护一个 deque （双端队列），保存这些孩子未满的节点。
    // 插入一个节点之后，该节点没有孩子，插入到 deque 的末尾。为了找到最小数字的节点，我们从 deque 头弹出元素。
    class CBTInserter {
        TreeNode root;
        Deque<TreeNode> deque;// 保存孩子未满的节点

        public CBTInserter(TreeNode root) {
            this.root = root;
            deque = new LinkedList<>();

            // 初始化：使用层序遍历该树，将孩子未满的节点放入双端队列
            Queue<TreeNode> queue = new LinkedList<>();// bfs辅助队列
            queue.offer(root);// 头节点入队

            while (!queue.isEmpty()) {
                TreeNode node = queue.poll();
                if (node.left == null || node.right == null)// 将孩子未满的节点放入双端队列
                    deque.offerLast(node);

                // bfs
                if (node.left != null)
                    queue.offer(node.left);
                if (node.right != null)
                    queue.offer(node.right);
            }
        }

        public int insert(int v) {
            TreeNode node = deque.peekFirst();// 第一个孩子未满的节点
            deque.offerLast(new TreeNode(v));
            if (node.left == null)// 左孩子为空
                node.left = deque.peekLast();
            else {// 右孩子为空，指向新节点之后，该节点左右孩子已满，出队
                node.right = deque.peekLast();
                deque.pollFirst();
            }
            return node.val;
        }

        public TreeNode get_root() {
            return root;
        }
    }

    // 方法二：（自己写的）计算路径-时间复杂度：O(nlogn)（实际跟队列保存孩子未满节点效率差不多），空间复杂度：O(n)
    class CBTInserter2 {
        TreeNode root;
        int nodeNum = 0;// 完全二叉树节点数

        public CBTInserter2(TreeNode root) {
            this.root = root;
            // 中序遍历完全二叉树，得到节点数
            dfs(root);
        }

        public int insert(int v) {
            // 栈存储路径，后进先出
            // 栈内布尔值表示：插入路径是否在左
            Deque<Boolean> path = new LinkedList<>();
            nodeNum++;
            int num = nodeNum;// 当前插入的节点是完全二叉树的第几个节点
            // 根据是第几个节点从后往前计算路径，为奇则在父节点右，为偶则在父节点左
            while (num != 1) {
                if ((num & 1) == 1)
                    path.push(false);
                else
                    path.push(true);
                num = num / 2;
            }

            // 根据路径找到插入节点的父节点
            TreeNode node = root;
            while (path.size() != 1) {
                boolean isLeft = path.pop();
                if (isLeft)
                    node = node.left;
                else
                    node = node.right;
            }

            // 插入节点
            boolean isLeft = path.pop();
            if (isLeft)
                node.left = new TreeNode(v);
            else
                node.right = new TreeNode(v);
            return node.val;
        }

        public TreeNode get_root() {
            return root;
        }

        private void dfs(TreeNode root) {
            if (root == null)
                return;
            dfs(root.left);
            nodeNum++;
            dfs(root.right);
        }
    }

    // 剑指Offer II 044.二叉树每层的最大值（主站515.）
    // 给定一棵二叉树的根节点 root ，请找出该二叉树中每一层的最大值。

    // 方法一：bfs（自己写的）-时空复杂度：O(n)
    public List<Integer> largestValues(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();// 辅助队列
        if (root != null)
            queue.offer(root);
        while (!queue.isEmpty()) {
            int n = queue.size();
            int max = Integer.MIN_VALUE;// 一层的最大值
            for (int i = 0; i < n; i++) {// 一层元素出队
                TreeNode node = queue.poll();
                max = Math.max(max, node.val);
                if (node.left != null)
                    queue.offer(node.left);
                if (node.right != null)
                    queue.offer(node.right);
            }
            res.add(max);
        }
        return res;
    }

    // 剑指Offer II 045.二叉树最底层最左边的值（主站513.）
    // 给定一个二叉树的 根节点 root，请找出该二叉树的 最底层 最左边 节点的值。
    // 假设二叉树中至少有一个节点。

    // 方法一：bfs（自己写的）-时空复杂度：O(n)
    public int findBottomLeftValue(TreeNode root) {
        int res = 0;
        Queue<TreeNode> queue = new LinkedList<>();
        if (root != null)
            queue.offer(root);
        while (!queue.isEmpty()) {
            int n = queue.size();
            for (int i = 0; i < n; i++) {// 一层元素出队
                if (i == 0)// 最左边的元素
                    res = queue.peek().val;
                TreeNode node = queue.poll();
                if (node.left != null)
                    queue.offer(node.left);
                if (node.right != null)
                    queue.offer(node.right);
            }
        }
        return res;
    }

    // 剑指Offer II 046.二叉树的右侧视图（主站199.）
    // 给定一个二叉树的 根节点 root，想象自己站在它的右侧，按照从顶部到底部的顺序，返回从右侧所能看到的节点值。

    // 方法一：深度优先搜索-时空复杂度：O(n)
    // 在搜索过程中，我们总是先访问右子树。那么对于每一层来说，我们在这层见到的第一个结点一定是最右边的结点。
    // 这样一来，我们可以存储在每个深度访问的第一个结点，一旦我们知道了树的层数，就可以得到最终的结果数组。
    public List<Integer> rightSideView(TreeNode root) {
        Map<Integer, Integer> rightmostValueAtDepth = new HashMap<Integer, Integer>();
        int max_depth = -1;

        Deque<TreeNode> nodeStack = new LinkedList<>();// 迭代dfs 辅助栈
        Deque<Integer> depthStack = new LinkedList<>();// 存储对应节点所在层数
        if (root != null) {
            nodeStack.push(root);
            depthStack.push(0);
        }

        while (!nodeStack.isEmpty()) {
            TreeNode node = nodeStack.pop();
            int depth = depthStack.pop();

            if (node != null) {
                max_depth = Math.max(max_depth, depth);// 维护二叉树的最大深度

                if (!rightmostValueAtDepth.containsKey(depth)) // 如果不存在对应深度的节点我们才插入（只记录每层访问的第一个节点）
                    rightmostValueAtDepth.put(depth, node.val);

                nodeStack.push(node.left);// 先进后出
                nodeStack.push(node.right);// 后进先出
                depthStack.push(depth + 1);
                depthStack.push(depth + 1);
            }
        }

        List<Integer> rightView = new ArrayList<Integer>();
        for (int depth = 0; depth <= max_depth; depth++)
            rightView.add(rightmostValueAtDepth.get(depth));

        return rightView;
    }

    // 方法二：广度优先搜索-时空复杂度：O(n)
    // 思路
    // 我们可以对二叉树进行层次遍历，那么对于每层来说，最右边的结点一定是最后被遍历到的。二叉树的层次遍历可以用广度优先搜索实现。
    public List<Integer> rightSideView2(TreeNode root) {
        Map<Integer, Integer> rightmostValueAtDepth = new HashMap<Integer, Integer>();// 层数：节点值
        int max_depth = -1;

        // 成对
        Queue<TreeNode> nodeQueue = new LinkedList<TreeNode>();// bfs辅助队列
        Queue<Integer> depthQueue = new LinkedList<Integer>();// 存储对应节点所在层数
        if (root != null) {
            nodeQueue.offer(root);
            depthQueue.offer(0);
        }

        while (!nodeQueue.isEmpty()) {
            TreeNode node = nodeQueue.poll();
            int depth = depthQueue.poll();

            max_depth = Math.max(max_depth, depth);// 维护二叉树的最大深度
            rightmostValueAtDepth.put(depth, node.val);// 由于每一层最后一个访问到的节点才是我们要的答案，因此不断更新对应深度的信息即可

            if (node.left != null) {
                nodeQueue.offer(node.left);
                depthQueue.offer(depth + 1);
            }
            if (node.right != null) {
                nodeQueue.offer(node.right);
                depthQueue.offer(depth + 1);
            }
        }

        List<Integer> rightView = new ArrayList<Integer>();
        for (int depth = 0; depth <= max_depth; depth++)
            rightView.add(rightmostValueAtDepth.get(depth));

        return rightView;
    }

    // 方法二：自己写的bfs-时空复杂度：O(n)
    public List<Integer> rightSideView3(TreeNode root) {
        List<Integer> res = new ArrayList<>();
        Queue<TreeNode> queue = new LinkedList<>();
        if (root != null)
            queue.offer(root);
        while (!queue.isEmpty()) {
            int n = queue.size();
            for (int i = 0; i < n; i++) {// 一层元素出队
                TreeNode node = queue.poll();
                if (i == n - 1)// 最右边的元素
                    res.add(node.val);
                if (node.left != null)
                    queue.offer(node.left);
                if (node.right != null)
                    queue.offer(node.right);
            }
        }
        return res;
    }

    // 剑指Offer II 047.二叉树剪枝（主站814.）
    // 给定一个二叉树 根节点 root ，树的每个节点的值要么是 0，要么是 1。请剪除该二叉树中所有节点的值为 0 的子树。
    // 节点 node 的子树为 node 本身，以及所有 node 的后代。

    // 方法一：dfs 递归-时空复杂度：O(n)
    // 由下往上剪枝（请剪除该二叉树中所有节点的值为 0 的子树）
    // 剪枝一定要通过将父节点节点的left right置为空才行，其他dfs迭代 bfs不好操作删除节点的父节点
    public TreeNode pruneTree(TreeNode root) {
        if (root == null)
            return null;
        root.left = pruneTree(root.left); // 左子树剪枝，得到剪枝后左子树
        root.right = pruneTree(root.right); // 右子树剪枝，得到剪枝后右子树

        if (root.left == null && root.right == null && root.val == 0)// 判断决定root结点是否需要剪掉（将复杂的剪枝情况统一为一种）
            return null;

        return root;// 返回root这棵树剪枝后的结果（自己）
    }

    // 剑指Offer II 048.序列化与反序列化二叉树（主站297.hot100 offer都有）
    // 序列化是将一个数据结构或者对象转换为连续的比特位的操作，进而可以将转换后的数据存储在一个文件或者内存中，
    // 同时也可以通过网络传输到另一个计算机环境，采取相反方式重构得到原数据。
    // 请设计一个算法来实现二叉树的序列化与反序列化。
    // 这里不限定你的序列 / 反序列化算法执行逻辑，只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

    // 方法一：DFS（先序遍历）-时间复杂度、空间复杂度：O(n)
    // 先序遍历，String用,隔开，null用None表示，再按先序遍历反序列化
    // 含有所有null的先序遍历，是可以还原出树的结构的
    // String类型是引用数据类型，但是因为有字符串常量池，做参数是值的复制
    // StringBuilder是引用数据类型，做参数是传递地址
    String serialize(TreeNode root) {
        StringBuilder sb = new StringBuilder("");
        rserialize(root, sb);
        return new String(sb);
    }

    void rserialize(TreeNode root, StringBuilder str) {
        if (root == null)
            str.append("None,");
        else {
            str.append(String.valueOf(root.val) + ",");// 先序遍历
            rserialize(root.left, str);
            rserialize(root.right, str);
        }

    }

    TreeNode deserialize(String data) {
        String[] dataArray = data.split(",");
        Queue<String> dataList = new LinkedList<>(Arrays.asList(dataArray));// 数组转List，如果就要使用数组，需要传入index指向当前节点位置
        return rdeserialize(dataList);
    }

    TreeNode rdeserialize(Queue<String> dataList) {
        if ("None".equals(dataList.peek())) {
            dataList.poll();
            return null;
        }
        // 按先序遍历反序列化
        TreeNode root = new TreeNode(Integer.parseInt(dataList.peek()));
        dataList.poll();
        root.left = rdeserialize(dataList);
        root.right = rdeserialize(dataList);
        return root;
    }

    // 方法二：（自己写的）bfs-时间复杂度、空间复杂度：O(n)
    public String serialize2(TreeNode root) {
        StringBuilder sb = new StringBuilder();
        // 辅助队列实现bfs遍历
        Queue<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.poll();
            if (node == null)
                sb.append("null,");
            else {
                sb.append(node.val);
                sb.append(",");
                queue.offer(node.left);
                queue.offer(node.right);
            }
        }
        sb.deleteCharAt(sb.length() - 1);
        return sb.toString();
    }

    public TreeNode deserialize2(String data) {
        Queue<String> datas = new LinkedList<>(Arrays.asList(data.split(",")));
        // 辅助队列queue根据队列datas重建二叉树
        Queue<TreeNode> queue = new LinkedList<>();
        TreeNode root = stringToTreeNode(datas.poll());
        queue.offer(root);
        while (!queue.isEmpty()) {
            // 对于每一个节点，出队时找到其左右孩子，左右孩子入队
            TreeNode node = queue.poll();
            if (node != null) {
                TreeNode left = stringToTreeNode(datas.poll());
                TreeNode right = stringToTreeNode(datas.poll());
                node.left = left;
                node.right = right;
                queue.offer(left);
                queue.offer(right);
            }
        }
        return root;
    }

    // String转TreeNode
    private TreeNode stringToTreeNode(String str) {
        if ("null".equals(str))
            return null;
        else
            return new TreeNode(Integer.valueOf(str));
    }

    // 方法三：括号表示编码（后序遍历，String是中序遍历顺序）+ 递归下降解码
    // 时间复杂度、空间复杂度：O(n) 为什么时间缩短10倍
    // 也可以这样表示一颗二叉树：
    // 如果当前的树为空，则表示为 X
    // 如果当前的树不为空，则表示为 (左子树序列化之后的结果)当前节点val(右子树序列化之后的结果)
    // 根据这样的定义，很好写出序列化的过程，「后序遍历」这颗二叉树即可，像方法1那样从左至右序列化不行
    // 那如何反序列化呢？根据定义，我们可以推导出这样的巴科斯范式（BNF）：
    // T -> (T) num (T) | X
    // 它的意义是：用 T 代表一棵树序列化之后的结果，| 表示 T 的构成为 (T) num (T) 或者 X
    // | 左边是对 T的递归定义，右边规定了递归终止的边界条件
    String serialize3(TreeNode root) {
        return rserialize3(root);
    }

    String rserialize3(TreeNode root) {
        if (root == null)
            return "X";

        return "(" + rserialize3(root.left) + ")" + root.val + "(" + rserialize3(root.right) + ")";
    }

    TreeNode deserialize3(String data) {
        int[] ptr = { 0 };// 为了不创建类的成员变量，（基本数据类型传入数据的复制，引用数据类型传入地址），用包装类也不行
        return parse(data, ptr);
    }

    TreeNode parse(String data, int[] ptr) {
        if (data.charAt(ptr[0]) == 'X') {
            ++ptr[0];
            return null;
        }
        TreeNode cur = new TreeNode(0);
        ++ptr[0]; // 跳过左括号
        cur.left = parse(data, ptr);// 左子树
        ++ptr[0]; // 跳过右括号

        cur.val = parseInt(data, ptr);// 当前节点值

        ++ptr[0]; // 跳过左括号
        cur.right = parse(data, ptr);// 右子树
        ++ptr[0]; // 跳过右括号
        return cur;
    }

    int parseInt(String data, int[] ptr) {
        int x = 0, sgn = 1;// 节点值有正负，-1000 <= Node.val <= 1000
        if (!Character.isDigit(data.charAt(ptr[0]))) {// 数字第一位是-
            sgn = -1;
            ++ptr[0];
        }
        while (Character.isDigit(data.charAt(ptr[0])))
            x = x * 10 + data.charAt(ptr[0]++) - '0';

        return x * sgn;
    }

    // 剑指Offer II 049.从根节点到叶节点的路径数字之和（主站129.）
    // 给定一个二叉树的根节点 root ，树中每个节点都存放有一个 0 到 9 之间的数字。
    // 每条从根节点到叶节点的路径都代表一个数字：
    // 例如，从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
    // 计算从根节点到叶节点生成的 所有数字之和 。
    // 叶节点 是指没有子节点的节点。
    // 提示：
    // 树中节点的数目在范围 [1, 1000] 内
    // 0 <= Node.val <= 9
    // 树的深度不超过 10

    // 方法一：dfs（最终答案随返回值传递）-时空复杂度：O(n)
    public int sumNumbers(TreeNode root) {
        return dfs(root, 0);
    }

    public int dfs(TreeNode root, int prevSum) {
        if (root == null) // 兄弟节点必不为空
            return 0;

        int sum = prevSum * 10 + root.val;
        if (root.left == null && root.right == null) // 左右子树同为空
            return sum;
        else
            return dfs(root.left, sum) + dfs(root.right, sum);// 其实就是后序，减少中间变量的创建，减低空间复杂度
    }

    // 方法一：（自己写的）dfs（最终答案设为全局变量）-时空复杂度：O(n)
    // int sum = 0;
    public int sumNumbers11(TreeNode root) {
        dfs49(root, 0);
        return sum;
    }

    private void dfs49(TreeNode root, int cur) {
        if (root == null)
            return;

        cur = cur * 10 + root.val;

        // 判断root是否为叶子节点，如果是，直接加入sum，且不用再dfs遍历
        if (root.left == null && root.right == null) {
            sum += cur;
            return;
        }
        dfs49(root.left, cur);
        dfs49(root.right, cur);
    }

    // 方法二：bfs-时空复杂度：O(n)
    public int sumNumbers2(TreeNode root) {
        if (root == null)
            return 0;

        int sum = 0;// 记录最终答案
        Queue<TreeNode> nodeQueue = new LinkedList<TreeNode>();// 存储节点
        Queue<Integer> numQueue = new LinkedList<Integer>();// 存储节点的累加值
        // 成对
        nodeQueue.offer(root);
        numQueue.offer(root.val);
        while (!nodeQueue.isEmpty()) {
            TreeNode node = nodeQueue.poll();
            int num = numQueue.poll();
            TreeNode left = node.left, right = node.right;
            if (left == null && right == null) // 一条完整路径
                sum += num;
            else {

                if (left != null) {
                    nodeQueue.offer(left);
                    numQueue.offer(num * 10 + left.val);
                }
                if (right != null) {
                    nodeQueue.offer(right);
                    numQueue.offer(num * 10 + right.val);
                }

            }
        }
        return sum;
    }

    // 剑指Offer II 050.向下的路径节点之和（主站437. hot100有）
    // 给定一个二叉树的根节点 root ，和一个整数 targetSum ，求该二叉树里节点值之和等于 targetSum 的 路径 的数目。
    // 路径 不需要从根节点开始，也不需要在叶子节点结束，但是路径方向必须是向下的（只能从父节点到子节点）。
    // 提示:
    // 二叉树的节点个数的范围是 [0,1000]
    // -109 <= Node.val <= 109
    // -1000 <= targetSum <= 1000
    // 对每个节点进行dfs，时间复杂度太高

    // 方法一：前缀和 dfs、回溯（自顶向下）-时间复杂度、空间复杂度：O(n)
    // 其实就是作差的思想
    // 在同一个路径之下（可以理解成二叉树从root节点出发，到叶子节点的某一条路径），
    // 如果前缀总和currSum，在节点A和节点B处相差target，则位于节点A和节点B之间的元素之和是target。

    int target;
    Map<Integer, Integer> prefixSumCount = new HashMap<>();// key是前缀和, value是大小为key的前缀和出现的次数

    public int pathSum(TreeNode root, int sum) {
        target = sum;
        prefixSumCount.put(0, 1);// 前缀和为0的一条路径
        return recursionPathSum(root, 0);// 前缀和的递归回溯思路
    }

    /**
     * 前缀和的递归回溯思路 从当前节点反推到根节点(反推比较好理解，正向其实也只有一条)，有且仅有一条路径，因为这是一棵树
     * 如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了
     * 所以前缀和对于当前路径来说是唯一的，当前记录的前缀和，在回溯结束，回到本层时去除，保证其不影响其他分支的结果
     * 
     * @param node           树节点
     * @param prefixSumCount 前缀和Map
     * @param target         目标值
     * @param currSum        当前路径和
     * @return 满足题意的解
     */
    private int recursionPathSum(TreeNode node, int currSum) {
        // 1.递归终止条件
        if (node == null)
            return 0;
        // 2.本层要做的事情
        int res = 0;

        currSum += node.val;// 当前路径上的和

        // ---核心代码
        // 看看root到当前节点这条路上是否存在节点前缀和加target为currSum的路径
        // 当前节点->root节点反推，有且仅有一条路径，「如果此前有和为currSum-target,而当前的和又为currSum,两者的差就肯定为target了」
        // currSum-target相当于找路径的起点，起点的sum+target=currSum，当前点到起点的距离就是target
        res += prefixSumCount.getOrDefault(currSum - target, 0);// 路径数
        prefixSumCount.put(currSum, prefixSumCount.getOrDefault(currSum, 0) + 1);// 更新路径上当前节点前缀和的个数
        // ---核心代码

        // 3.进入下一层
        res += recursionPathSum(node.left, currSum);
        res += recursionPathSum(node.right, currSum);

        // 4.回到本层，恢复状态，去除当前节点的前缀和数量（DFS离开当前节点）
        prefixSumCount.put(currSum, prefixSumCount.get(currSum) - 1);
        return res;
    }

    // 剑指Offer II 051.节点之和最大的路径（主站124.hot100有）
    // 路径 被定义为一条从树中任意节点出发，沿父节点-子节点连接，达到任意节点的序列。
    // 同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点，且不一定经过根节点。
    // 路径和 是路径中各节点值的总和。
    // 给定一个二叉树的根节点 root ，返回其 最大路径和，即所有路径上节点值之和的最大值。

    // 方法一：dfs 递归 后序-时间复杂度、空间复杂度：O(n)
    int maxSum = Integer.MIN_VALUE;

    int maxPathSum(TreeNode root) {
        maxGain(root);// 没必要再接收根节点返回值了
        return maxSum;
    }

    int maxGain(TreeNode node) {
        if (node == null)
            return 0;

        // 递归计算左右子节点的最大贡献值，只有在最大贡献值大于 0 时，才会选取对应子节点
        // 贡献为负，则不要子节点的贡献，取0（精髓之一，非常巧妙）
        int leftGain = Math.max(maxGain(node.left), 0);
        int rightGain = Math.max(maxGain(node.right), 0);
        int priceNewpath = node.val + leftGain + rightGain;// 节点的最大路径和取决于该节点的值与该节点的左右子节点的最大贡献值
        maxSum = Math.max(maxSum, priceNewpath); // 更新答案
        return node.val + Math.max(leftGain, rightGain);// 返回节点的最大贡献值（这里可能为负）
    }

    // 剑指Offer II 052.展平二叉搜索树

    // 剑指Offer II 053.二叉搜索树中的中序后继（主站285.）
    // 给定一棵二叉搜索树和其中的一个节点 p ，找到该节点在树中的中序后继。如果节点没有中序后继，请返回 null 。
    // 节点 p 的后继是值比 p.val 大的节点中键值最小的节点，即按中序遍历的顺序节点 p 的下一个节点。

    // 方法一：利用二叉搜索树的性质-时空复杂度：O(logn)
    // 最后一跳进入右子树退出循环，中序后继是最后一次进入左子树的节点
    // 最后一跳进入左子树退出循环，中序后继是其父节点
    public TreeNode inorderSuccessor(TreeNode root, TreeNode p) {
        TreeNode cur = root;
        TreeNode result = null;
        while (cur != null) {
            if (cur.val > p.val) {// 当前节点值大于p，记录该节点（可能是p的后继），进入左子树（寻找尽可能小的后继节点）
                result = cur;
                cur = cur.left;
            } else// 当前节点值小于等于p，进入右子树（寻找更大的节点）
                cur = cur.right;
        }
        return result;
    }

    // 方法二：dfs 递归（自己写的，时间好像也差不多）-时空复杂度：O(n)
    // 不是二叉搜索树也行
    boolean findP = false;
    TreeNode res;

    public TreeNode inorderSuccessor2(TreeNode root, TreeNode p) {
        dfs(root, p);
        return res;
    }

    public void dfs(TreeNode root, TreeNode p) {
        if (root == null)
            return;

        dfs(root.left, p);
        if (findP) {// 找到p后的第一个节点即为中序后继
            res = root;
            findP = false;// 防止记录后面节点
        }
        if (root == p)
            findP = true;
        dfs(root.right, p);
    }

    // 剑指Offer II 054.所有大于等于节点的值之和（主站538.hot100有 1038.）
    // 给定一个二叉搜索树，请将它的每个节点的值替换成树中大于或者等于该节点值的所有节点值之和。
    // 提醒一下，二叉搜索树满足下列约束条件：
    // 节点的左子树仅包含键 小于 节点键的节点。
    // 节点的右子树仅包含键 大于 节点键的节点。
    // 左右子树也必须是二叉搜索树。

    // 方法一：反序中序遍历-空间复杂度-O(n)
    int sum = 0;

    public TreeNode convertBST(TreeNode root) {
        if (root != null) {
            convertBST(root.right);// 先右
            sum += root.val;// 加后赋值
            root.val = sum;
            convertBST(root.left);// 后左
        }
        return root;
    }

    // 方法二：Morris 遍历-空间复杂度-O(1)
    // Morris 遍历的核心思想是利用树的大量空闲指针，实现空间开销的极限缩减。其「反序中序遍历」规则总结如下：
    // 1.如果当前节点的右子节点为空，处理当前节点，并遍历当前节点的左子节点；
    // 2.如果当前节点的右子节点不为空，找到当前节点右子树的最左节点（该节点为当前节点中序遍历的前驱节点）；
    // 如果最左节点的左指针为空，将最左节点的左指针指向当前节点，遍历当前节点的右子节点；
    // 如果最左节点的左指针不为空，将最左节点的左指针重新置为空（恢复树的原状），处理当前节点，并将当前节点置为其左节点；
    // 3.重复步骤 1 和步骤 2，直到遍历结束。
    public TreeNode convertBST2(TreeNode root) {
        int sum = 0;
        TreeNode node = root;

        while (node != null) {
            if (node.right == null) {
                sum += node.val;
                node.val = sum;
                node = node.left;
            } else {
                TreeNode succ = getSuccessor(node);
                if (succ.left == null) {
                    succ.left = node;
                    node = node.right;
                } else {
                    succ.left = null;
                    sum += node.val;
                    node.val = sum;
                    node = node.left;
                }
            }
        }

        return root;
    }

    public TreeNode getSuccessor(TreeNode node) {
        TreeNode succ = node.right;
        while (succ.left != null && succ.left != node)
            succ = succ.left;

        return succ;
    }

    // 剑指Offer II 055.二叉搜索树迭..

    // 剑指Offer II 056.二叉搜索树中两个节点之和（主站653.）
    // 给定一个二叉搜索树的 根节点 root 和一个整数 k , 请判断该二叉搜索树中是否存在两个节点它们的值之和等于 k 。
    // 假设二叉搜索树中节点的值均唯一。

    // 方法一：dfs + 哈希表，时空复杂度：O(n)
    // 先序遍历
    public boolean findTarget(TreeNode root, int k) {
        Set<Integer> set = new HashSet<>();
        return find(root, k, set);
    }

    public boolean find(TreeNode root, int k, Set<Integer> set) {
        if (root == null)
            return false;
        if (set.contains(k - root.val))
            return true;
        set.add(root.val);
        return find(root.left, k, set) || find(root.right, k, set);
    }

    // 方法一：dfs + 哈希表（自己写的，提前剪枝，实际效率差不多），时空复杂度：O(n)
    Set<Integer> set = new HashSet<>();

    public boolean findTarget11(TreeNode root, int k) {
        if (root == null)
            return false;

        if (findTarget(root.left, k))
            return true;

        if (set.contains(k - root.val))
            return true;
        set.add(root.val);

        if (findTarget(root.right, k))
            return true;

        return false;
    }

    // 方法二：bfs
    // 方法三：利用bst的特性，遍历放入集合 + 双指针

    // 剑指Offer II 059.数据流的第 K 大数值（主站703.）
    // 设计一个找到数据流中第 k 大元素的类（class）。注意是排序后的第 k 大元素，不是第 k 个不同的元素。
    // 请实现 KthLargest 类：
    // KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
    // int add(int val) 将 val 插入数据流 nums 后，返回当前数据流中第 k 大的元素。
    // 题目数据保证，在查找第 k 大元素时，数组中至少有 k 个元素（算上该次插入）

    // 方法一：优先队列
    // 我们可以使用一个大小为 k 的优先队列来存储前 k 大的元素，其中优先队列的队头为队列中最小的元素，也就是第 k 大的元素。
    class KthLargest {
        PriorityQueue<Integer> pq;
        int k;

        public KthLargest(int k, int[] nums) {
            this.k = k;
            pq = new PriorityQueue<Integer>();
            for (int x : nums)
                add(x);
        }

        public int add(int val) {
            // 这种写法值得学习，充分利用堆的性质，从而减少代码量
            pq.offer(val);
            while (pq.size() > k) // 保证堆中只有k个元素
                pq.poll();

            return pq.peek();
        }
    }

}

// 深度优先遍历 + 广度优先遍历
class DFSBFSOfferII {
    class Node {
        public int val;
        public Node prev;
        public Node next;
        public Node child;
    }

    // 剑指Offer II 028.展平多级双向链表（主站430.）
    // 多级双向链表中，除了指向下一个节点和前一个节点指针之外，它还有一个子链表指针，可能指向单独的双向链表。
    // 这些子列表也可能会有一个或多个自己的子项，依此类推，生成多级数据结构，如下面的示例所示。
    // 给定位于列表第一级的头节点，请扁平化列表，即将这样的多级双向链表展平成普通的双向链表，使所有结点出现在单级双链表中。
    // （优先排列child，无child排next）

    // 方法一：dfs 递归-时间复杂度：O(n)，空间复杂度：O(n)
    // 优先展平底层链表
    public Node flatten(Node head) {
        dfs(head);
        return head;
    }

    public Node dfs(Node node) {
        Node cur = node;
        Node last = null;// 记录链表的最后一个节点
        while (cur != null) {// 遍历至null，结束该层链表的循环
            Node next = cur.next;

            // 如果有子节点，那么首先处理子节点
            if (cur.child != null) {
                Node childLast = dfs(cur.child);// 进入下一层链表，得到下一层链表的末尾节点
                next = cur.next;// 记录cur节点的next节点

                // 将 cur 与 child 相连
                cur.next = cur.child;
                cur.child.prev = cur;

                // 如果 next 不为空，就将 last 与 next 相连
                if (next != null) {
                    childLast.next = next;
                    next.prev = childLast;
                }
                cur.child = null;// 将 cur 的 child 置为空
                last = childLast;// 记录末尾节点
            } else
                last = cur;// 记录末尾节点

            cur = next;
        }
        return last;
    }

    // 方法一：（自己写的，虽然这样栈的深度可能更深，但更符合常规的递归思路）dfs 递归-时间复杂度：O(n)，空间复杂度：O(n)
    public Node flatten11(Node head) {
        if (head == null)
            return head;
        dfs1(head);
        return head;
    }

    private Node dfs1(Node head) {
        if (head.child != null) {
            Node next = head.next;

            head.next = head.child;
            head.child = null;
            head.next.prev = head;

            Node curr = dfs1(head.next);// 下层末尾节点
            if (next != null) {// 精髓之一：有child的节点的next若为null，则不需要处理了，直接返回下一层的末尾节点
                curr.next = next;
                next.prev = curr;
                curr = dfs1(next);
            }
            return curr;
        } else if (head.next == null)
            return head;
        else
            return dfs1(head.next);
    }

    // 方法二：迭代-时间复杂度：O(n)，空间复杂度：O(1)
    // 优先展平顶层链表
    // 该方法可不使用栈结构辅助存储next节点
    public Node flatten2(Node head) {
        Node dummy = new Node();
        dummy.next = head;
        for (; head != null; head = head.next) {
            if (head.child != null) {
                Node next = head.next;// 当前节点的next节点
                Node child = head.child;// 当前节点的child节点

                // 连接当前节点和child
                head.next = child;
                child.prev = head;
                head.child = null;

                // 找到末尾节点
                Node last = head;
                // 循环里套循环，这样可以省去使用栈存储next节点
                // 即把处理child的那一层看做是一个整体，提前把末尾与上层next相连
                while (last.next != null)
                    last = last.next;

                last.next = next; // 连接末尾节点和next节点
                if (next != null)
                    next.prev = last;
            }
        }
        return dummy.next;
    }

    // 方法二：（自己写的）迭代-时间复杂度：O(n)，空间复杂度：O(n)
    public Node flatten22(Node head) {
        Node res = head;
        Deque<Node> stk = new LinkedList<>();
        while (head != null || !stk.isEmpty()) {
            if (head.child != null) {
                if (head.next != null)// 精髓之一：有child的节点的next若为null，则不需要处理了（不入栈）
                    stk.push(head.next);

                head.next = head.child;
                head.child = null;
                head.next.prev = head;
                head = head.next;
            } else if (head.next == null) {
                if (stk.isEmpty())// 这里可以直接返回了
                    return res;

                Node next = stk.pop();
                head.next = next;
                next.prev = head;
                head = head.next;
            } else
                head = head.next;
        }
        return res;
    }

    // 剑指Offer II 044.二叉树每层的..
    // 剑指Offer II 045.二叉树最底层...
    // 剑指Offer II 046.二叉树的右侧..
    // 剑指Offer II 047.二叉树剪枝
    // 剑指Offer II 048.序列化与反序...
    // 剑指Offer II 049.从根节点到叶..
    // 剑指Offer II 050.向下的路径节...
    // 剑指Offer II 051.节点之和最大...
    // 剑指Offer II 052.展平二叉搜索树
    // 剑指Offer II 053.二叉搜索树中...
    // 剑指Offer II 054.所有大于等于...
    // 剑指Offer II 056.二叉搜索树中...

    // 剑指Offer II 086.分割回文子字符串（主站131.）
    // 给定一个字符串 s ，请将 s 分割成一些子串，使每个子串都是 回文串 ，返回 s 所有可能的分割方案。
    // 回文串 是正着读和反着读都一样的字符串。

    // 方法一：回溯 + 动态规划预处理-时间复杂度：O(n2^n)，空间复杂度：O(n^2)
    boolean[][] f;// 动态规划预处理 [i, j]是否是回文串
    List<List<String>> ret = new ArrayList<List<String>>();// 所有情况
    List<String> ans = new ArrayList<String>();// 一种情况
    int n;// 总字符串长度

    public String[][] partition(String s) {
        n = s.length();

        // 动态规划预处理 [i, j]是否是回文串
        f = new boolean[n][n];
        for (int i = 0; i < n; ++i)
            Arrays.fill(f[i], true);// 单字符 [i, i] 为回文串

        // 注意确定左右边界的顺序，避免重复计算
        for (int i = n - 1; i >= 0; --i)
            for (int j = i + 1; j < n; ++j)
                f[i][j] = (s.charAt(i) == s.charAt(j)) && f[i + 1][j - 1];

        dfs(s, 0);

        // List<List<String>> 转 String[][]，这里不重要
        String[][] array = new String[ret.size()][];
        for (int i = 0; i < ret.size(); i++) {
            array[i] = new String[ret.get(i).size()];
            for (int j = 0; j < array[i].length; j++)
                array[i][j] = ret.get(i).get(j);
        }
        return array;
    }

    // 起始索引i
    public void dfs(String s, int i) {
        if (i == n) {
            ret.add(new ArrayList<String>(ans));// 复制一份放入最终答案
            return;
        }

        for (int j = i; j < n; ++j)
            if (f[i][j]) {
                ans.add(s.substring(i, j + 1));// 放入答案
                dfs(s, j + 1);
                ans.remove(ans.size() - 1);// 还原
            }
    }

    // 方法一：（自己写的）回溯 + 预处理判断回文串-时间复杂度：O(n2^n)，空间复杂度：O(n^2)
    String s;
    // int n;
    boolean[][] isPalindrome;
    // List<String> ans = new ArrayList<>();
    List<List<String>> res = new ArrayList<>();

    public String[][] partition11(String s) {
        this.s = s;
        this.n = s.length();
        this.isPalindrome = new boolean[n][n];

        for (int i = 0; i < n; i++) {
            findPalindrome(i, i);
            findPalindrome(i, i + 1);
        }

        dfs86(0);
        String[][] r = new String[res.size()][];
        for (int i = 0; i < res.size(); i++) {
            List<String> temp = res.get(i);
            r[i] = temp.toArray(new String[temp.size()]);
        }
        return r;

    }

    private void dfs86(int index) {
        if (index == n) {
            res.add(new ArrayList<>(ans));
            return;
        }

        for (int end = index; end < n; end++) {
            if (isPalindrome[index][end]) {
                ans.add(s.substring(index, end + 1));
                dfs(end + 1);
                ans.remove(ans.size() - 1);
            }
        }
    }

    private void findPalindrome(int left, int right) {
        while (left >= 0 && right < n) {
            if (s.charAt(left) == s.charAt(right))
                isPalindrome[left][right] = true;
            else
                break;
            left--;
            right++;
        }
    }

    // 剑指Offer II 105.岛屿的最大面积（主站695.）
    // 给定一个由 0 和 1 组成的非空二维数组 grid ，用来表示海洋岛屿地图。
    // 一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合，这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。
    // 你可以假设 grid 的四个边缘都被 0（代表水）包围着。
    // 找到给定的二维数组中最大的岛屿面积。如果没有岛屿，则返回面积为 0 。

    // 方法一：dfs 递归-时间复杂度：O(mn)，空间复杂度：O(mn)
    public int maxAreaOfIsland(int[][] grid) {
        int ans = 0;
        for (int i = 0; i < grid.length; ++i)
            for (int j = 0; j < grid[0].length; ++j)
                ans = Math.max(ans, dfs(grid, i, j));
        return ans;
    }

    public int dfs(int[][] grid, int cur_i, int cur_j) {
        // 越界，或者节点为0
        if (cur_i < 0 || cur_j < 0 || cur_i == grid.length || cur_j == grid[0].length || grid[cur_i][cur_j] == 0)
            return 0;

        grid[cur_i][cur_j] = 0;// 遍历后置0，后续不会再遍历该点
        int[] di = { 0, 0, 1, -1 };
        int[] dj = { 1, -1, 0, 0 };
        int ans = 1;
        for (int index = 0; index != 4; ++index) {// 依次遍历四个方向
            int next_i = cur_i + di[index], next_j = cur_j + dj[index];
            ans += dfs(grid, next_i, next_j);
        }
        return ans;
    }

    // 方法二：dfs 迭代

    // 方法三：bfs-时间复杂度：O(mn)，空间复杂度：O(mn)
    public int maxAreaOfIsland3(int[][] grid) {
        int ans = 0;
        for (int i = 0; i != grid.length; ++i)
            for (int j = 0; j != grid[0].length; ++j) {// 对每个节点进行依次bfs
                int cur = 0;// 当前岛屿面积
                Queue<int[]> queue = new LinkedList<int[]>();
                queue.offer(new int[] { i, j });
                while (!queue.isEmpty()) {
                    int[] cur_ij = queue.poll();
                    int cur_i = cur_ij[0], cur_j = cur_ij[1];
                    // 越界，或者节点为0
                    if (cur_i < 0 || cur_j < 0 || cur_i == grid.length || cur_j == grid[0].length
                            || grid[cur_i][cur_j] == 0)
                        continue;
                    ++cur;
                    grid[cur_i][cur_j] = 0;// 遍历后置0，后续不会再遍历该点
                    int[] di = { 0, 0, 1, -1 };
                    int[] dj = { 1, -1, 0, 0 };
                    for (int index = 0; index != 4; ++index) {// 四个方向节点依次入队
                        int next_i = cur_i + di[index], next_j = cur_j + dj[index];
                        queue.offer(new int[] { next_i, next_j });
                    }
                }
                ans = Math.max(ans, cur);
            }
        return ans;
    }

    // 剑指Offer II 106.二分图（主站785.）
    // 存在一个 无向图 ，图中有 n 个节点。其中每个节点都有一个介于 0 到 n - 1 之间的唯一编号。
    // 给定一个二维数组 graph ，表示图，其中 graph[u] 是一个节点数组，由节点 u 的邻接节点组成。
    // 形式上，对于 graph[u] 中的每个 v ，都存在一条位于节点 u 和节点 v 之间的无向边。该无向图同时具有以下属性：
    // 不存在自环（graph[u] 不包含 u）。
    // 不存在平行边（graph[u] 不包含重复值）。
    // 如果 v 在 graph[u] 内，那么 u 也应该在 graph[v] 内（该图是无向图）
    // 这个图可能不是连通图，也就是说两个节点 u 和 v 之间可能不存在一条连通彼此的路径。

    // 二分图 定义：如果能将一个图的节点集合分割成两个独立的子集 A 和 B ，并使图中的每一条边的两个节点
    // 一个来自 A 集合，一个来自 B 集合，就将这个图称为 二分图 。
    // 如果图是二分图，返回 true ；否则，返回 false 。

    // 染色法，思路：如果给定的无向图连通，那么我们就可以任选一个节点开始，给它染成红色。
    // 随后我们对整个图进行遍历，将该节点直接相连的所有节点染成绿色，表示这些节点不能与起始节点属于同一个集合。
    // 我们再将这些绿色节点直接相连的所有节点染成红色，以此类推，直到无向图中的每个节点均被染色。

    // 方法一：dfs-时间复杂度：O(N+M)，其中 N 和 M 分别是无向图中的点数和边数，空间复杂度：O(N)
    private static final int UNCOLORED = 0;
    private static final int RED = 1;
    private static final int GREEN = 2;
    private int[] color;// 每个节点对应的颜色
    private boolean valid;// 是否为二分图

    public boolean isBipartite(int[][] graph) {
        int n = graph.length;// 节点数
        valid = true;
        color = new int[n];// 每个节点对应的颜色
        Arrays.fill(color, UNCOLORED);
        for (int i = 0; i < n && valid; ++i)// dfs遍历每个节点染成红色（可能存在节点不连通）
            if (color[i] == UNCOLORED)
                dfs(i, RED, graph);

        return valid;
    }

    /**
     * 
     * @param node  节点索引
     * @param c     颜色
     * @param graph 图
     */
    public void dfs(int node, int c, int[][] graph) {
        color[node] = c;
        int cNei = (c == RED) ? GREEN : RED;// 相邻节点应当染成的颜色
        for (int neighbor : graph[node]) {// 遍历相邻节点进行染色
            if (color[neighbor] == UNCOLORED) {// 相邻节点未染色
                dfs(neighbor, cNei, graph);
                if (!valid)// 提前返回
                    return;
            } else if (color[neighbor] == c) {// 相邻节点染色，且为同色
                valid = false;
                return;
            }
        }
    }

    // 方法二：bfs
    // private static final int UNCOLORED = 0;
    // private static final int RED = 1;
    // private static final int GREEN = 2;
    // private int[] color;
    public boolean isBipartite2(int[][] graph) {
        int n = graph.length;// 节点数
        color = new int[n];// 每个节点对应的颜色
        Arrays.fill(color, UNCOLORED);
        for (int i = 0; i < n; ++i) // bfs遍历每个节点（可能存在节点不连通）
            if (color[i] == UNCOLORED) {
                Queue<Integer> queue = new LinkedList<Integer>();// 辅助队列，存放节点索引
                queue.offer(i);
                color[i] = RED;// 初始节点染成红色
                while (!queue.isEmpty()) {
                    int node = queue.poll();
                    int cNei = color[node] == RED ? GREEN : RED;// 相邻节点应当染成的颜色
                    for (int neighbor : graph[node]) {
                        if (color[neighbor] == UNCOLORED) {// 相邻节点未染色
                            queue.offer(neighbor);
                            color[neighbor] = cNei;
                        } else if (color[neighbor] == color[node])// 相邻节点染色，且为同色
                            return false;
                    }
                }
            }
        return true;
    }

    // 剑指 Offer II 108. 单词演变（主站127.）
    // 在字典（单词列表） wordList 中，从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列：
    // 序列中第一个单词是 beginWord 。
    // 序列中最后一个单词是 endWord 。
    // 每次转换只能改变一个字母。
    // 转换过程中的中间单词必须是字典 wordList 中的单词。
    // 给定两个长度相同但内容不同的单词 beginWord 和 endWord 和一个字典 wordList ，
    // 找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列，返回 0。
    // 提示：
    // 1 <= beginWord.length <= 10
    // endWord.length == beginWord.length
    // 1 <= wordList.length <= 5000
    // wordList[i].length == beginWord.length
    // beginWord、endWord 和 wordList[i] 由小写英文字母组成
    // beginWord != endWord
    // wordList 中的所有字符串 互不相同

    // 方法一：广度优先搜索 + 优化建图-时间复杂度：O(nC2)，C 为列表中单词的长度，空间复杂度：O(nC2)
    // 本题要求的是最短转换序列的长度，看到最短首先想到的就是广度优先搜索。

    class jsolution_108_1 {
        int nodeNum = 0;
        Map<String, Integer> wordId = new HashMap<String, Integer>();// 词与索引的映射关系
        List<List<Integer>> edge = new ArrayList<List<Integer>>();// 图
        // 也可以直接Map<String, List<String>> edge; 这样不需要建立词与索引的映射关系，但效率会降低

        public int ladderLength(String beginWord, String endWord, List<String> wordList) {
            // 先给每一个单词标号，即给每个单词分配一个 id。
            // 创建一个由单词 word 到 id 对应的映射 wordId，并将 beginWord 与 wordList 中所有的单词都加入这个映射中。
            // 之后我们检查 endWord 是否在该映射内，若不存在，则输入无解。我们可以使用哈希表实现上面的映射关系。

            // 然后我们需要建图，依据朴素的思路，我们可以枚举每一对单词的组合，判断它们是否恰好相差一个字符，以判断这两个单词对应的节点是否能够相连。
            // 但是这样效率太低，我们可以优化建图。

            // 具体地，我们可以创建虚拟节点。
            // 对于单词 hit，我们创建三个虚拟节点 *it、h*t、hi*，并让 hit 向这三个虚拟节点分别连一条边即可。
            // 如果一个单词能够转化为 hit，那么该单词必然会连接到这三个虚拟节点之一。
            // 对于每一个单词，我们枚举它连接到的虚拟节点，把该单词对应的 id 与这些虚拟节点对应的 id 相连即可。
            for (String word : wordList)
                addEdge(word);
            addEdge(beginWord);
            if (!wordId.containsKey(endWord))
                return 0;

            // 最后我们将起点加入队列开始广度优先搜索，当搜索到终点时，我们就找到了最短路径的长度。
            // 注意因为添加了虚拟节点，所以我们得到的距离为实际最短路径长度的两倍。
            // 同时我们并未计算起点对答案的贡献，所以我们应当返回距离的一半再加一的结果。
            int[] dis = new int[nodeNum];// 起始节点到每个节点的最短路
            Arrays.fill(dis, Integer.MAX_VALUE);
            int beginId = wordId.get(beginWord), endId = wordId.get(endWord);
            dis[beginId] = 0;

            Queue<Integer> que = new LinkedList<Integer>();
            que.offer(beginId);
            while (!que.isEmpty()) {
                int x = que.poll();
                if (x == endId)// 第一个到终止节点
                    return dis[endId] / 2 + 1;

                for (int it : edge.get(x))
                    if (dis[it] == Integer.MAX_VALUE) {// 第一次访问时，入队，更新dis
                        dis[it] = dis[x] + 1;
                        que.offer(it);
                    }
            }
            return 0;
        }

        public void addEdge(String word) {
            addWord(word);
            int id1 = wordId.get(word);
            char[] array = word.toCharArray();
            int length = array.length;
            // 生成虚拟节点，并插入图
            for (int i = 0; i < length; ++i) {
                char tmp = array[i];
                array[i] = '*';
                String newWord = new String(array);
                addWord(newWord);
                int id2 = wordId.get(newWord);
                edge.get(id1).add(id2);
                edge.get(id2).add(id1);
                array[i] = tmp;
            }
        }

        public void addWord(String word) {
            if (!wordId.containsKey(word)) {
                wordId.put(word, nodeNum++);
                edge.add(new ArrayList<Integer>());
            }
        }
    }

    // 方法二：双向广度优先搜索
    // 根据给定字典构造的图可能会很大，而广度优先搜索的搜索空间大小依赖于每层节点的分支数量。
    // 假如每个节点的分支数量相同，搜索空间会随着层数的增长指数级的增加。
    // 如果使用两个同时进行的广搜可以有效地减少搜索空间。一边从 beginWord 开始，另一边从 endWord 开始。
    // 我们每次从两边各扩展一层节点，当发现某一时刻两边都访问过同一顶点时就停止搜索。

    class jsolution_108_2 {
        Map<String, Integer> wordId = new HashMap<String, Integer>();
        List<List<Integer>> edge = new ArrayList<List<Integer>>();
        int nodeNum = 0;

        public int ladderLength(String beginWord, String endWord, List<String> wordList) {
            // 建图
            for (String word : wordList)
                addEdge(word);
            addEdge(beginWord);
            if (!wordId.containsKey(endWord))
                return 0;

            // 从起始节点开始bfs
            int[] disBegin = new int[nodeNum];
            Arrays.fill(disBegin, Integer.MAX_VALUE);
            int beginId = wordId.get(beginWord);
            disBegin[beginId] = 0;
            Queue<Integer> queBegin = new LinkedList<Integer>();
            queBegin.offer(beginId);

            // 从终止节点开始bfs
            int[] disEnd = new int[nodeNum];
            Arrays.fill(disEnd, Integer.MAX_VALUE);
            int endId = wordId.get(endWord);
            disEnd[endId] = 0;
            Queue<Integer> queEnd = new LinkedList<Integer>();
            queEnd.offer(endId);

            while (!queBegin.isEmpty() && !queEnd.isEmpty()) {
                // 从起始节点开始bfs
                int queBeginSize = queBegin.size();
                for (int i = 0; i < queBeginSize; ++i) {
                    int nodeBegin = queBegin.poll();
                    if (disEnd[nodeBegin] != Integer.MAX_VALUE)// 从终止节点开始的bfs已访问过该节点
                        return (disBegin[nodeBegin] + disEnd[nodeBegin]) / 2 + 1;

                    for (int it : edge.get(nodeBegin))
                        if (disBegin[it] == Integer.MAX_VALUE) {
                            disBegin[it] = disBegin[nodeBegin] + 1;
                            queBegin.offer(it);
                        }

                }

                // 从终止节点开始bfs
                int queEndSize = queEnd.size();
                for (int i = 0; i < queEndSize; ++i) {
                    int nodeEnd = queEnd.poll();
                    if (disBegin[nodeEnd] != Integer.MAX_VALUE)// 从起始节点开始的bfs已访问过该节点
                        return (disBegin[nodeEnd] + disEnd[nodeEnd]) / 2 + 1;

                    for (int it : edge.get(nodeEnd))
                        if (disEnd[it] == Integer.MAX_VALUE) {
                            disEnd[it] = disEnd[nodeEnd] + 1;
                            queEnd.offer(it);
                        }
                }
            }
            return 0;
        }

        public void addEdge(String word) {
            addWord(word);
            int id1 = wordId.get(word);
            char[] array = word.toCharArray();
            int length = array.length;
            for (int i = 0; i < length; ++i) {
                char tmp = array[i];
                array[i] = '*';
                String newWord = new String(array);
                addWord(newWord);
                int id2 = wordId.get(newWord);
                edge.get(id1).add(id2);
                edge.get(id2).add(id1);
                array[i] = tmp;
            }
        }

        public void addWord(String word) {
            if (!wordId.containsKey(word)) {
                wordId.put(word, nodeNum++);
                edge.add(new ArrayList<Integer>());
            }
        }
    }

    // 方法三：（自己写的）dfs + 记忆化搜索

    // 剑指 Offer II 109. 开密码锁（主站752.）
    // 一个密码锁由 4 个环形拨轮组成，
    // 每个拨轮都有 10 个数字： '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。
    // 每个拨轮可以自由旋转：例如把 '9' 变为 '0'，'0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。
    // 锁的初始数字为 '0000' ，一个代表四个拨轮的数字的字符串。
    // 列表 deadends 包含了一组死亡数字，一旦拨轮的数字和列表里的任何一个元素相同，这个锁将会被永久锁定，无法再被旋转。
    // 字符串 target 代表可以解锁的数字，请给出解锁需要的最小旋转次数，如果无论如何不能解锁，返回 -1 。

    // 方法一：bfs
    // 最多10000种情况
    public int openLock(String[] deadends, String target) {
        if ("0000".equals(target))
            return 0;
        Set<String> dead = new HashSet<String>();// 哈希表存储 deadends 中的所有元素
        for (String deadend : deadends)
            dead.add(deadend);
        if (dead.contains("0000"))
            return -1;

        int step = 0;
        Queue<String> queue = new LinkedList<String>();
        queue.offer("0000");
        Set<String> seen = new HashSet<String>();// 标记访问过的status
        seen.add("0000");
        while (!queue.isEmpty()) {
            ++step;
            int size = queue.size();
            for (int i = 0; i < size; ++i) {
                String status = queue.poll();
                for (String nextStatus : get(status))// 相邻status
                    if (!seen.contains(nextStatus) && !dead.contains(nextStatus)) {// 未访问过，且不是deadend
                        if (nextStatus.equals(target))
                            return step;
                        queue.offer(nextStatus);
                        seen.add(nextStatus);
                    }
            }
        }

        return -1;
    }

    // 枚举 status 通过一次旋转得到的数字
    public List<String> get(String status) {
        List<String> ret = new ArrayList<String>();
        char[] array = status.toCharArray();
        for (int i = 0; i < 4; ++i) {
            char num = array[i];
            array[i] = numPrev(num);
            ret.add(new String(array));
            array[i] = numSucc(num);
            ret.add(new String(array));
            array[i] = num;// 还原
        }
        return ret;
    }

    public char numPrev(char x) {
        return x == '0' ? '9' : (char) (x - 1);
    }

    public char numSucc(char x) {
        return x == '9' ? '0' : (char) (x + 1);
    }

    // 方法一：（自己写的）bfs
    public int openLock11(String[] deadends, String target) {
        Set<String> setDeadends = new HashSet<>();
        for (String deadend : deadends)
            setDeadends.add(deadend);
        if (setDeadends.contains("0000"))
            return -1;

        int[] res = new int[10000];
        Arrays.fill(res, -1);
        res[0] = 0;
        Queue<String> queue = new ArrayDeque<>();
        queue.offer("0000");
        while (!queue.isEmpty()) {
            String currString = queue.poll();
            List<String> nextStrings = getNext(currString);
            for (String nextString : nextStrings) {
                if (setDeadends.contains(nextString))
                    continue;
                int currIndex = Integer.valueOf(currString);
                int nextIndex = Integer.valueOf(nextString);
                if (res[nextIndex] != -1)
                    continue;
                res[nextIndex] = res[currIndex] + 1;
                if (nextString.equals(target))
                    return res[nextIndex];
                queue.offer(nextString);
            }
        }
        return res[Integer.valueOf(target)];
    }

    private List<String> getNext(String curr) {
        List<String> ans = new ArrayList<>();
        char[] currChars = null;
        for (int i = 0; i < 4; i++) {
            currChars = curr.toCharArray();
            if (currChars[i] == '9')
                currChars[i] = '0';
            else
                currChars[i] = (char) (currChars[i] + 1);
            ans.add(String.valueOf(currChars));

            currChars = curr.toCharArray();
            if (currChars[i] == '0')
                currChars[i] = '9';
            else
                currChars[i] = (char) (currChars[i] - 1);
            ans.add(String.valueOf(currChars));
        }
        return ans;
    }

    // 方法二：启发式搜索
    // （bfs的改进，使用pq替换原来的辅助队列，即不按入队顺序bfs，而是有优先地遍历）
    // 启发式搜索能更快地找到最小旋转次数。这里我们可以使用 A* 算法。
    // 在 A* 算法中，我们需要使用四个距离函数 F(x), G(x), H(x), H*(x)
    // 其中 F(x), G(x), H(x) 是可以求出的，而 H∗(x) 是无法求出的，我们需要用 H(x) 近似 H^*(x)。
    // 设起点为 s，终点为 t，这些距离函数的意义如下：
    // G(x) 表示从起点 s 到节点 x 的「实际」路径长度，注意 G(x) 并不一定是最短的；
    // H(x) 表示从节点 x 到终点 t 的「估计」最短路径长度，称为启发函数；
    // H∗(x) 表示从节点 x 到终点 t 的「实际」最短路径长度，这是我们在广度优先搜索的过程中无法求出的，我们需要用 H(x) 近似 H∗(x)；
    // F(x) 满足 F(x) = G(x) + H(x)，即为从起点 s 到终点 t 的「估计」路径长度。
    // 我们总是挑选出最小的 F(x) 对应的 x 进行搜索，因此 A* 算法需要借助优先队列来实现。
    // 如果读者熟悉求解最短路的 Dijkstra 算法，就可以发现 Dijkstra 算法是 A* 算法在H(x)≡0 时的特殊情况。

    public int openLock2(String[] deadends, String target) {
        if ("0000".equals(target))
            return 0;
        Set<String> dead = new HashSet<String>();
        for (String deadend : deadends)
            dead.add(deadend);
        if (dead.contains("0000"))
            return -1;

        PriorityQueue<AStar> pq = new PriorityQueue<AStar>((a, b) -> a.f - b.f);// 小根堆
        pq.offer(new AStar("0000", target, 0));
        Set<String> seen = new HashSet<String>();
        seen.add("0000");
        while (!pq.isEmpty()) {
            AStar node = pq.poll();
            for (String nextStatus : get(node.status))
                if (!seen.contains(nextStatus) && !dead.contains(nextStatus)) {
                    if (nextStatus.equals(target))
                        return node.g + 1;
                    pq.offer(new AStar(nextStatus, target, node.g + 1));
                    seen.add(nextStatus);
                }
        }
        return -1;
    }

    class AStar {
        String status;
        int f, g, h;

        public AStar(String status, String target, int g) {
            this.status = status;
            this.g = g;
            this.h = getH(status, target);
            this.f = this.g + this.h;
        }

        // 计算启发函数
        // 将 status 的第 i 个数字旋转到与 target 的第 i 个数字一致，最少需要的次数
        // 即 H(status) 等于不存在死亡数字时最小的旋转次数。
        public int getH(String status, String target) {
            int ret = 0;
            for (int i = 0; i < 4; ++i) {
                int dist = Math.abs(status.charAt(i) - target.charAt(i));
                ret += Math.min(dist, 10 - dist);
            }
            return ret;
        }
    }
    // public char numPrev(char x) {
    // return x == '0' ? '9' : (char) (x - 1);
    // }

    // public char numSucc(char x) {
    // return x == '9' ? '0' : (char) (x + 1);
    // }

    // 枚举 status 通过一次旋转得到的数字
    // public List<String> get(String status) {
    // List<String> ret = new ArrayList<String>();
    // char[] array = status.toCharArray();
    // for (int i = 0; i < 4; ++i) {
    // char num = array[i];
    // array[i] = numPrev(num);
    // ret.add(new String(array));
    // array[i] = numSucc(num);
    // ret.add(new String(array));
    // array[i] = num;
    // }
    // return ret;
    // }

    // 剑指Offer II 110.所有路径
    // 给定一个有 n 个节点的有向无环图，用二维数组 graph 表示，请找到所有从 0 到 n-1 的路径并输出（不要求按顺序）。
    // graph 的第 i 个数组中的单元都表示有向图中 i 号节点所能到达的下一些结点（译者注：有向图是有方向的，即规定了 a→b 你就不能从 b→a ），
    // 若为空，就是没有下一个节点了。
    // 提示：
    // n == graph.length
    // 2 <= n <= 15
    // 0 <= graph[i][j] < n
    // graph[i][j] != i 
    // 保证输入为有向无环图 (GAD)
    // （搜索过程中不会反复遍历同一个点，因此我们无需判断当前点是否遍历过。）

    // 方法一：dfs-时间复杂度：O(n 2^n)，其中 n 为图中点的数量，空间复杂度：O(n)
    List<List<Integer>> ans110 = new ArrayList<List<Integer>>();
    Deque<Integer> onePath = new ArrayDeque<Integer>();// 一条路径

    public List<List<Integer>> allPathsSourceTarget(int[][] graph) {
        onePath.offerLast(0);
        dfs110(graph, 0, graph.length - 1);
        return ans110;
    }

    /**
     * 
     * @param graph
     * @param x     当前节点索引
     * @param n     终点
     */
    public void dfs110(int[][] graph, int x, int n) {
        if (x == n) {
            ans110.add(new ArrayList<Integer>(onePath));// 复制放入最终答案
            return;
        }
        for (int y : graph[x]) { // 遍历所有相邻节点
            onePath.offerLast(y);
            dfs110(graph, y, n);
            onePath.pollLast();// dfs结束记得还原
        }
    }

    // 剑指Offer II 111.计算除法（主站399. hot100有）
    // 给定一个变量对数组 equations 和一个实数值数组 values 作为已知条件，
    // 其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi
    // 是一个表示单个变量的字符串。
    // 另有一些以数组 queries 表示的问题，其中 queries[j] = [Cj, Dj] 表示第 j 个问题，请你根据已知条件找出 Cj / Dj =
    // ? 的结果作为答案。
    // 返回 所有问题的答案 。如果存在某个无法确定的答案，则用 -1.0 替代这个答案。
    // 如果问题中出现了给定的已知条件中没有出现的字符串，也需要用 -1.0 替代这个答案。
    // 注意：输入总是有效的。可以假设除法运算中不会出现除数为 0 的情况，且不存在任何矛盾的结果。
    // 提示：
    // 1 <= equations.length <= 20
    // equations[i].length == 2
    // 1 <= Ai.length, Bi.length <= 5
    // values.length == equations.length
    // 0.0 < values[i] <= 20.0
    // 1 <= queries.length <= 20
    // queries[i].length == 2
    // 1 <= Cj.length, Dj.length <= 5
    // Ai, Bi, Cj, Dj 由小写英文字母与数字组成

    // 1.（带权）并查集-时间复杂度：O((N+Q)log A)
    // 构建并查集 O(NlogA) ，这里 N 为输入方程 equations 的长度，
    // 每一次执行合并操作的时间复杂度是 O(logA)，这里 A 是 equations 里不同字符的个数；
    // 查询并查集 O(QlogA)，这里 Q 为查询数组 queries 的长度，
    // 每一次查询时执行「路径压缩」的时间复杂度是 O(logA)。
    // 空间复杂度：O(A)：创建字符与 id 的对应关系 hashMap 长度为 A，
    // 并查集底层使用的两个数组 parent 和 weight 存储每个变量的连通分量信息，parent 和 weight 的长度均为 A。

    // 由于 变量之间的倍数关系具有传递性，处理有传递性关系的问题，可以使用「并查集」，
    // 我们需要在并查集的「合并」与「查询」操作中 维护这些变量之间的倍数关系。
    public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
        int equationsSize = equations.size();

        // equationsSize个等式，则最多2 * equationsSize个变量
        CalcEquationUnionFind unionFind = new CalcEquationUnionFind(2 * equationsSize);

        // 第 1 步：预处理，将变量的值与 id 进行映射，使得并查集的底层使用数组实现，方便编码
        Map<String, Integer> hashMap = new HashMap<>(2 * equationsSize);// 指定initialCapacity
        int id = 0;
        for (int i = 0; i < equationsSize; i++) {// 处理第i个式子
            List<String> equation = equations.get(i);
            String var1 = equation.get(0);// 被除数
            String var2 = equation.get(1);// 除数

            if (!hashMap.containsKey(var1)) {
                hashMap.put(var1, id);
                id++;
            }
            if (!hashMap.containsKey(var2)) {
                hashMap.put(var2, id);
                id++;
            }
            unionFind.union(hashMap.get(var1), hashMap.get(var2), values[i]);// 将两个变量对应的id传入，进行合并
        }

        // 第 2 步：做查询
        int queriesSize = queries.size();
        double[] res = new double[queriesSize];
        for (int i = 0; i < queriesSize; i++) {// 计算第i个式子
            String var1 = queries.get(i).get(0);
            String var2 = queries.get(i).get(1);

            Integer id1 = hashMap.get(var1);// 获取被除数对应的id
            Integer id2 = hashMap.get(var2);// 获取除数对应的id

            if (id1 == null || id2 == null)// 只要有一个不存在id（即，字符在equations中未出现）
                res[i] = -1.0d;
            else
                res[i] = unionFind.isConnected(id1, id2);

        }
        return res;
    }

    private class CalcEquationUnionFind {// 内部类

        private int[] parent;// 各节点的父节点

        private double[] weight;// 指向的父节点的权值（除法的值）

        public CalcEquationUnionFind(int n) {// 初始化，n个变量
            this.parent = new int[n];
            this.weight = new double[n];
            for (int i = 0; i < n; i++) {
                parent[i] = i;// 初始化时，每个节点的父节点都默认是自己
                weight[i] = 1.0d;// 除法（自己除以自己）值为1
            }
        }

        public void union(int x, int y, double value) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY)// 两节点父节点为同一个，则无需合并
                return;

            parent[rootX] = rootY;// 被除数的父节点为除数

            weight[rootX] = weight[y] * value / weight[x];// 节点x / 节点x的父节点 = weight[x]
        }

        // 路径压缩，快速找到父节点
        public int find(int x) {
            if (x != parent[x]) {
                int origin = parent[x];
                parent[x] = find(parent[x]);// 修改父节点指向，指向父节点的父节点
                weight[x] *= weight[origin];// 权值乘以父节点权值
            }
            return parent[x];
        }

        public double isConnected(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX == rootY)// x, y父节点相同
                return weight[x] / weight[y]; // (x / x, y共同的父节点) / (y / x, y共同的父节点) = x / y
            else
                return -1.0d;// 不在一个集合里，则无法通过已有等式计算得到

        }
    }

    // 2.bfs-时间复杂度：O(ML+Q(L+M))
    // 其中 M 为边的数量，Q 为询问的数量，L 为字符串的平均长度。
    // 构建图时，需要处理 M 条边，每条边都涉及到 O(L) 的字符串比较；
    // 处理查询时，每次查询首先要进行一次 O(L) 的比较，然后至多遍历 O(M) 条边。
    // 空间复杂度：O(NL+M)，其中 N 为点的数量，M 为边的数量，L 为字符串的平均长度。
    // 为了将每个字符串映射到整数，需要开辟空间为 O(NL) 的哈希表；随后，需要花费 O(M) 的空间存储每条边的权重；
    // 处理查询时，还需要 O(N) 的空间维护访问队列。
    // 最终，总的复杂度为 O(NL+M+N) =O(NL+M)
    public double[] calcEquation2(List<List<String>> equations, double[] values, List<List<String>> queries) {
        // 将变量的值与 id 进行映射，使得并查集的底层使用数组实现，方便编码
        Map<String, Integer> variables = new HashMap<String, Integer>();
        int nvars = 0;
        int n = equations.size();
        for (int i = 0; i < n; i++) {
            if (!variables.containsKey(equations.get(i).get(0)))
                variables.put(equations.get(i).get(0), nvars++);

            if (!variables.containsKey(equations.get(i).get(1)))
                variables.put(equations.get(i).get(1), nvars++);
            // 相比于并查集，并查集会在映射的同时，对节点进行合并，后续查询时会更快，时间复杂度O(n)
            // bfs的查询，每一个式子都要一次bfs
        }

        // 对于每个点，存储其直接连接到的所有点及对应的权值
        @SuppressWarnings("unchecked")
        List<Pair>[] edges = new ArrayList[nvars];// 相比无权图，需要用一个Pair存储权值和邻接边的信息
        for (int i = 0; i < nvars; i++)
            edges[i] = new ArrayList<Pair>();

        for (int i = 0; i < n; i++) {// 每一个式子：a / b
            int va = variables.get(equations.get(i).get(0)), vb = variables.get(equations.get(i).get(1));
            edges[va].add(new Pair(vb, values[i]));// a / b
            edges[vb].add(new Pair(va, 1.0 / values[i]));// b / a
        }

        int queriesCount = queries.size();
        double[] ret = new double[queriesCount];
        for (int i = 0; i < queriesCount; i++) {// 查询求解，每一个式子都要一次bfs
            List<String> query = queries.get(i);
            double result = -1.0;// 没找到解的默认返回
            if (variables.containsKey(query.get(0)) && variables.containsKey(query.get(1))) {// 除数和被除数同时存在映射
                int ia = variables.get(query.get(0)), ib = variables.get(query.get(1));
                if (ia == ib)
                    result = 1.0;
                else {// bfs
                    Queue<Integer> points = new LinkedList<Integer>();
                    points.offer(ia);// 入队
                    double[] ratios = new double[nvars];// 每次bfs都要重复计算，遍历到该点x时，赋值：a / x = a / 中间点 * 中间点 / x
                    // Assigns the specified double value (-1.0) to each element of the specified
                    // array
                    Arrays.fill(ratios, -1.0);
                    ratios[ia] = 1.0;// a / a = 1.0

                    while (!points.isEmpty() && ratios[ib] < 0) {// ratios[ib]不为0则表示已找到最终答案，可提前退出循环
                        int x = points.poll();
                        for (Pair pair : edges[x]) {
                            int y = pair.index;
                            double val = pair.value;
                            if (ratios[y] < 0) {// 此时y未遍历到，已遍历过则不入队
                                ratios[y] = ratios[x] * val;
                                points.offer(y);// 入队
                            }
                        }
                    }
                    result = ratios[ib];// 如果bfs未遍历到ib，则默认值-1.0，若遍历到，则为最终答案
                }
            }
            ret[i] = result;
        }
        return ret;
    }

    private class Pair {// 内部类
        int index;// 邻接边的索引
        double value;// 权值

        Pair(int index, double value) {
            this.index = index;
            this.value = value;
        }
    }

    // 3. 自己写的lowb dfs-时间复杂度：O(ML+Q(L+M))
    // 空间复杂度：O(NL+M)
    boolean findQuery;// 是否已经找到最终答案
    double[] ratios;// 被除数 / 所有节点
    int ib;// 每个求解式子的除数

    public double[] calcEquation3(List<List<String>> equations, double[] values, List<List<String>> queries) {
        // 将变量的值与 id 进行映射，使得并查集的底层使用数组实现，方便编码
        Map<String, Integer> variables = new HashMap<String, Integer>();
        int nvars = 0;
        int n = equations.size();
        for (int i = 0; i < n; i++) {
            if (!variables.containsKey(equations.get(i).get(0)))
                variables.put(equations.get(i).get(0), nvars++);

            if (!variables.containsKey(equations.get(i).get(1)))
                variables.put(equations.get(i).get(1), nvars++);
            // 相比于并查集，并查集会在映射的同时，对节点进行合并，后续查询时会更快，时间复杂度O(n)
            // bfs的查询，每一个式子都要一次bfs
        }
        // 对于每个点，存储其直接连接到的所有点及对应的权值
        @SuppressWarnings("unchecked")
        List<Pair>[] edges = new ArrayList[nvars];// 相比无权图，需要用一个Pair存储权值和邻接边的信息
        for (int i = 0; i < nvars; i++)
            edges[i] = new ArrayList<Pair>();
        for (int i = 0; i < n; i++) {// 每一个式子：a / b
            int va = variables.get(equations.get(i).get(0)), vb = variables.get(equations.get(i).get(1));
            edges[va].add(new Pair(vb, values[i]));// a / b
            edges[vb].add(new Pair(va, 1.0 / values[i]));// b / a
        }

        int queriesCount = queries.size();
        double[] ret = new double[queriesCount];
        for (int i = 0; i < queriesCount; i++) {// 查询求解，每一个式子都要一次dfs
            List<String> query = queries.get(i);
            double result = -1.0;// 没找到解的默认返回
            if (variables.containsKey(query.get(0)) && variables.containsKey(query.get(1))) {// 除数和被除数同时存在映射
                int ia = variables.get(query.get(0));// 被除数a
                ib = variables.get(query.get(1)); // 除数b
                if (ia == ib)
                    result = 1.0;
                else {// dfs
                    findQuery = false;
                    ratios = new double[nvars];// 每次dfs都要重复计算，遍历到该点x时，赋值：a / x = a / 中间点 * 中间点 / x
                    // Assigns the specified double value (-1.0) to each element of the specified
                    // array
                    Arrays.fill(ratios, -1.0);
                    ratios[ia] = 1.0;// a / a = 1.0
                    dfs(edges, ia);
                    result = ratios[ib];// 如果bfs未遍历到ib，则默认值-1.0，若遍历到，则为最终答案
                }
            }
            ret[i] = result;
        }
        return ret;
    }

    void dfs(List<Pair>[] edges, int x) {
        if (findQuery)// 提前结束循环
            return;

        for (Pair p : edges[x]) {
            int y = p.index;// 节点x的邻接节点
            if (y == ib) {
                findQuery = true;
                ratios[ib] = ratios[x] * p.value;// a/x * x/b
                return;
            }
            if (ratios[y] < 0) {// 未遍历到
                ratios[y] = ratios[x] * p.value;// a/x * x/y
                dfs(edges, y);
            }
            if (findQuery)// 提前结束循环
                return;
        }

    }
    // 4.Floyd算法-时间复杂度：O(ML+N3+QL)。
    // 构建图需要 O(ML) 的时间；Floyd 算法需要O(N3) 的时间；处理查询时，单次查询只需要 O(L) 的字符串比较以及常数时间的额外操作。
    // 空间复杂度：O(NL+N2)。

    // 对于查询数量很多的情形，如果为每次查询都独立搜索一次，则效率会变低。为此，我们不妨对图先做一定的预处理，随后就可以在较短的时间内回答每个查询。
    // 在本题中，我们可以使用 Floyd 算法，预先计算出任意两点之间的距离。
    public double[] calcEquation4(List<List<String>> equations, double[] values, List<List<String>> queries) {
        int nvars = 0;
        Map<String, Integer> variables = new HashMap<String, Integer>();// String 到 int 的映射

        int n = equations.size();
        for (int i = 0; i < n; i++) {
            if (!variables.containsKey(equations.get(i).get(0)))
                variables.put(equations.get(i).get(0), nvars++);

            if (!variables.containsKey(equations.get(i).get(1)))
                variables.put(equations.get(i).get(1), nvars++);

        }

        double[][] graph = new double[nvars][nvars];// Floyd算法中使用邻接矩阵来表示图的边
        for (int i = 0; i < nvars; i++)
            Arrays.fill(graph[i], -1.0);// 初值赋为-1.0表示两点不邻接

        for (int i = 0; i < n; i++) {
            int va = variables.get(equations.get(i).get(0)), vb = variables.get(equations.get(i).get(1));
            graph[va][vb] = values[i];// 坐标(a, b)的权值表示 a / b
            graph[vb][va] = 1.0 / values[i];// (b, a) b / a
        }

        // floyd算法模板
        for (int k = 0; k < nvars; k++)// 存在一定量的重复计算
            for (int i = 0; i < nvars; i++)
                for (int j = 0; j < nvars; j++)
                    if (graph[i][k] > 0 && graph[k][j] > 0) // 节点 i 与 k 邻接且 k 与 j 邻接
                        graph[i][j] = graph[i][k] * graph[k][j];// 则可通过中间点 k 计算得到 i / j

        int queriesCount = queries.size();
        double[] ret = new double[queriesCount];
        for (int i = 0; i < queriesCount; i++) {
            List<String> query = queries.get(i);
            double result = -1.0;
            if (variables.containsKey(query.get(0)) && variables.containsKey(query.get(1))) {
                int ia = variables.get(query.get(0)), ib = variables.get(query.get(1));
                if (graph[ia][ib] > 0)
                    result = graph[ia][ib];

            }
            ret[i] = result;
        }
        return ret;
    }

    // 剑指Offer II 112.最长递增路径

    // 剑指Offer II 113.课程顺序（主站210. 207.拓扑排序（只判断） hot100有）
    // 现在总共有 numCourses 门课需要选，记为 0 到 numCourses-1。
    // 给定一个数组 prerequisites ，它的每一个元素 prerequisites[i] 表示两门课程之间的先修顺序。 
    // 例如 prerequisites[i] = [ai, bi] 表示想要学习课程 ai ，需要先完成课程 bi 。
    // 请根据给出的总课程数  numCourses 和表示先修顺序的 prerequisites 得出一个可行的修课序列。
    // 可能会有多个正确的顺序，只要任意返回一种就可以了。如果不可能完成所有课程，返回一个空数组。
    // 提示:
    // 1 <= numCourses <= 2000
    // 0 <= prerequisites.length <= numCourses * (numCourses - 1)
    // prerequisites[i].length == 2
    // 0 <= ai, bi < numCourses
    // ai != bi
    // prerequisites 中不存在重复元素

    // 方法一：bfs-时间复杂度、空间复杂度-O(n+m)
    List<List<Integer>> edges;// 存储有向图
    int[] indeg;// 存储每个节点的入度
    int[] result; // 存储答案
    int index;// 答案下标顺序写

    public int[] findOrder(int numCourses, int[][] prerequisites) {
        // 建图
        edges = new ArrayList<List<Integer>>();
        for (int i = 0; i < numCourses; ++i)
            edges.add(new ArrayList<Integer>());

        indeg = new int[numCourses];
        result = new int[numCourses];
        index = 0;

        // 计算入度
        for (int[] info : prerequisites) {
            edges.get(info[1]).add(info[0]);
            ++indeg[info[0]];
        }

        Queue<Integer> queue = new LinkedList<Integer>();
        // 将所有入度为 0 的节点放入队列中
        for (int i = 0; i < numCourses; ++i)
            if (indeg[i] == 0)
                queue.offer(i);

        while (!queue.isEmpty()) {
            int u = queue.poll();// 从队首取出一个节点
            result[index++] = u;// 放入答案中
            for (int v : edges.get(u)) {
                --indeg[v];
                if (indeg[v] == 0) // 如果相邻节点 v 的入度为 0，就可以选 v 对应的课程了
                    queue.offer(v);
            }
        }

        if (index != numCourses)// 是否访问到了所有节点（是否有环）
            return new int[0];

        return result;
    }

    // 方法二：dfs-时间复杂度、空间复杂度-O(n+m)
    // 逻辑有点绕，感觉还是bfs好理解（凭出入度为0选择节点）

    List<List<Integer>> edges2;// 存储有向图，前指向后
    int[] visited;// 标记每个节点的状态：0=未搜索，1=搜索中，2=已完成
    // int[] result;// 用数组来模拟栈，下标 n-1 为栈底，0 为栈顶
    // boolean valid = true;// 判断有向图中是否有环
    // int index;// 栈下标，倒着写

    public int[] findOrder2(int numCourses, int[][] prerequisites) {
        // 建图
        edges2 = new ArrayList<>();
        for (int i = 0; i < numCourses; ++i)
            edges2.add(new ArrayList<Integer>());

        visited = new int[numCourses];
        result = new int[numCourses];
        index = numCourses - 1;
        for (int[] info : prerequisites)
            edges2.get(info[1]).add(info[0]);

        // 每次挑选一个「未搜索」的节点，开始进行dfs
        for (int i = 0; i < numCourses && valid; ++i)
            if (visited[i] == 0)
                dfs(i);

        if (!valid)
            return new int[0];

        return result;// 如果没有环，那么就有拓扑排序
    }

    public void dfs(int u) {
        // 将节点标记为「搜索中」
        visited[u] = 1;
        // 搜索其相邻节点
        // 只要发现有环，立刻停止搜索
        for (int v : edges2.get(u)) {
            // 如果「未搜索」那么搜索相邻节点
            if (visited[v] == 0) {
                dfs(v);
                if (!valid)
                    return;
            }
            // 如果「搜索中」说明找到了环
            else if (visited[v] == 1) {
                valid = false;
                return;
            }
        }
        visited[u] = 2;// 将节点标记为「已完成」
        result[index--] = u;// 将节点入栈
    }

    // 剑指Offer II 114.外星文字典（主站269.）
    // 现有一种使用英语字母的外星文语言，这门语言的字母顺序与英语顺序不同。
    // 给定一个字符串列表 words ，作为这门语言的词典，words 中的字符串已经 按这门新语言的字母顺序进行了排序 。
    // 请你根据该词典还原出此语言中已知的字母顺序，并 按字母递增顺序 排列。若不存在合法字母顺序，返回 "" 。
    // 若存在多种可能的合法字母顺序，返回其中 任意一种 顺序即可。
    // 字符串 s 字典顺序小于 字符串 t 有两种情况：
    // 在第一个不同字母处，如果 s 中的字母在这门外星语言的字母顺序中位于 t 中字母之前，那么 s 的字典顺序小于 t 。
    // 如果前面 min(s.length, t.length) 字母都相同，那么 s.length < t.length 时，s 的字典顺序也小于 t 。
    // 提示：
    // 1 <= words.length <= 100
    // 1 <= words[i].length <= 100
    // words[i] 仅由小写英文字母组成

    // 方法一：bfs 拓扑排序
    public String alienOrder(String[] words) {
        // 把出现的字符保存在图HashMap里，每个不重复字符对应一个表示有序的有向边
        Map<Character, Set<Character>> graph = new HashMap<>();// <c1, (c2, c3)> → c1 < c2, c1 < c3
        // List<Character> graph[] = new ArrayList[26];//也可以这样存储图，未出现的字符对应null
        int[] inDegree = new int[26];// 各字符节点入度
        Queue<Character> q = new LinkedList<>();
        StringBuilder sb = new StringBuilder();

        // 记录出现的字符
        for (String word : words)
            for (char ch : word.toCharArray())
                graph.putIfAbsent(ch, new HashSet<>());

        // 两两比较相邻字符串之间的关系，得到两个字符的相对大小关系
        for (int i = 1; i < words.length; ++i) {
            String w1 = words[i - 1];
            String w2 = words[i];
            if (!preCheck(w1, w2))// 不合法 e.g. abc < ab
                return "";
            for (int j = 0; j < Math.min(w1.length(), w2.length()); ++j) {
                char c1 = w1.charAt(j);
                char c2 = w2.charAt(j);
                if (c1 != c2) {// 找到不同的字符才说明有顺序关系，把前一个字符指向后一字符，同时后一字符的入度+1
                    if (!graph.get(c1).contains(c2)) {
                        graph.get(c1).add(c2);
                        inDegree[c2 - 'a']++;
                    }
                    break;
                }
            }
        }
        // 把所有入度为0的字符先加入队列，准备拓扑排序
        for (char ch : graph.keySet())
            if (inDegree[ch - 'a'] == 0)
                q.offer(ch);

        while (!q.isEmpty()) {
            // 从队列出来的这个字符肯定是入度为0，可以确定它的顺序，就把它加进字符顺序的结果里
            char node = q.poll();
            sb.append(node);

            // 该节点指向的所有节点的入度都要减1，若有入度为0的节点字符出现，把它加进队列准备之后的拓扑排序遍历
            for (char next : graph.get(node)) {
                inDegree[next - 'a']--;
                if (inDegree[next - 'a'] == 0)
                    q.offer(next);
            }
        }
        // 如果结果集里的字符数量和图中所有节点数量相同，说明拓扑排序成功，返回结果集，否则不成功，不存在合法字母顺序
        return sb.length() == graph.size() ? sb.toString() : "";
    }

    /**
     * 判断两字符大小关系是否合法（即最大前缀相同，且左边长度大于右边 e.g. abc < ab 不合法）
     * 
     * @param s1
     * @param s2
     * @return
     */
    public boolean preCheck(String s1, String s2) {
        int m = s1.length(), n = s2.length();
        if (m < n)// 左字符串长度小于右字符串长度，一定合法
            return true;
        int i = 0, j = 0;
        while (i < m && j < n) {
            if (s2.charAt(j) != s1.charAt(i))
                return true;
            i++;
            j++;
        }
        if (!s1.equals(s2))
            return false;
        else
            return true;
    }

    // 方法一：（自己写的）bfs 拓扑排序
    Map<Character, Integer> indegrees = new HashMap<>();
    Map<Character, List<Character>> graph = new HashMap<>();
    // 标记 word 之间出现了不可能的大小顺序，如 abc ab
    // boolean valid = true;

    public String alienOrder11(String[] words) {
        // 仅记录有哪些字符，初始化 indegrees graph
        for (String word : words)
            for (char c : word.toCharArray()) {
                if (!graph.containsKey(c))
                    graph.put(c, new ArrayList<>());
                if (!indegrees.containsKey(c))
                    indegrees.put(c, 0);
            }

        // 记录 word 两两之间的大小关系，构建图，记录入度
        for (int i = 0; i < words.length - 1; i++) {
            char[] order = getOrder(words[i], words[i + 1]);
            if (!valid)// 标记 word 之间出现了不可能的大小顺序，如 abc ab
                return "";
            if (order == null)
                continue;
            char c1 = order[0];
            char c2 = order[1];
            graph.get(c1).add(c2);
            indegrees.put(c2, indegrees.get(c2) + 1);
        }

        // 入度为0的字符加入队列
        Queue<Character> queue = new ArrayDeque<>();
        for (Character c : indegrees.keySet())
            if (indegrees.get(c) == 0)
                queue.offer(c);

        // bfs 遍历图，得到拓扑排序
        StringBuilder res = new StringBuilder();
        while (!queue.isEmpty()) {
            char c = queue.poll();
            res.append(c);
            for (char next : graph.get(c)) {
                indegrees.put(next, indegrees.get(next) - 1);
                if (indegrees.get(next) == 0)
                    queue.offer(next);
            }
        }

        // 检查res长度是否==总字符数
        if (res.length() == indegrees.size())
            return res.toString();
        else
            return "";
    }

    /**
     * 返回 word1 word2 在该顺序下，体现的两字符的大小关系
     * 若 无法体现字符之间的大小关系，如 ab abc 则返回null
     * 若 word 之间出现了不可能的大小顺序，如 abc ab，标志位 valid = false
     * 
     * @param word1
     * @param word2
     * @return
     */
    private char[] getOrder(String word1, String word2) {
        for (int i = 0; i < Math.min(word1.length(), word2.length()); i++)
            if (word1.charAt(i) != word2.charAt(i))
                return new char[] { word1.charAt(i), word2.charAt(i) };

        if (word1.length() > word2.length())
            valid = false;
        return null;
    }

    // 剑指 Offer II 115. 重建序列（主站444.）
    // 请判断原始的序列 org 是否可以从序列集 seqs 中唯一地 重建 。
    // 序列 org 是 1 到 n 整数的排列，其中 1 ≤ n ≤ 104。重建 是指在序列集 seqs 中构建最短的公共超序列，
    // 即  seqs 中的任意序列都是该最短序列的子序列。
    // 提示：
    // n == nums.length
    // 1 <= n <= 104
    // nums 是 [1, n] 范围内所有整数的排列
    // 1 <= sequences.length <= 104
    // 1 <= sequences[i].length <= 104 注意：sequence 长度不总为2，甚至可能为1
    // 1 <= sum(sequences[i].length) <= 105
    // 1 <= sequences[i][j] <= n
    // sequences 的所有数组都是 唯一 的
    // sequences[i] 是 nums 的一个子序列

    // 方法一：拓扑排序
    public boolean sequenceReconstruction(int[] org, List<List<Integer>> seqs) {
        if (seqs == null || seqs.size() == 0)
            return false;

        // 记录org中的所有元素
        Set<Integer> set = new HashSet<>();
        for (int ele : org)
            set.add(ele);
        for (List<Integer> seq : seqs) // 检查seqs中是否出现了org中没有的元素
            for (int num : seq)
                if (!set.contains(num))
                    return false;

        int n = org.length;
        // 建元素之间的有向边关系
        Map<Integer, Set<Integer>> graph = new HashMap<>();
        int[] in = new int[n + 1];// 记录元素的入度
        for (int i = 1; i <= n; ++i)
            graph.put(i, new HashSet<>());
        for (List<Integer> seq : seqs)
            for (int i = 0; i < seq.size() - 1; ++i)
                if (!graph.get(seq.get(i)).contains(seq.get(i + 1))) {// 图中没有：节点i指向节点i+1这条边
                    graph.get(seq.get(i)).add(seq.get(i + 1));
                    in[seq.get(i + 1)]++;
                }

        // 准备拓扑排序，入度为0的元素入队，注意要想确定唯一序列，队列中每次只可能有唯一入度为零的元素
        Queue<Integer> q = new LinkedList<>();
        for (int i = 1; i <= n; ++i)
            if (in[i] == 0)
                q.offer(i);

        List<Integer> ls = new ArrayList<>();// 记录拓扑排序

        if (q.size() != 1) // 入度为0的元素不唯一则无法确定唯一序列
            return false;
        while (!q.isEmpty()) {
            if (q.size() != 1)// 入度为0的节点大于1个，则无唯一序列
                return false;
            int cur = q.poll();
            ls.add(cur);
            if (graph.get(cur) == null)// 当前节点没有指向的节点了
                break;
            for (int next : graph.get(cur)) {
                in[next]--;
                if (in[next] == 0)
                    q.offer(next);
            }
        }

        if (ls.size() != n)// 比较出队序列的元素顺序，若和原始不一致则重建失败
            return false;
        for (int i = 0; i < n; ++i) // 长度一样则比较是否完全一样
            if (ls.get(i) != org[i])
                return false;
        return true;// 全部元素符合重建成功
    }

    // 方法一：（自己写的）拓扑排序
    // 输入跟以前不一样了
    public boolean sequenceReconstruction11(int[] nums, int[][] sequences) {
        Map<Integer, Set<Integer>> graph = new HashMap<>();
        Map<Integer, Integer> indegrees = new HashMap<>();

        for (int[] sequence : sequences) {
            if (sequence.length == 1 && !graph.containsKey(sequence[0])) {
                graph.put(sequence[0], new HashSet<>());
                indegrees.put(sequence[0], 0);
            }

            for (int i = 0; i < sequence.length - 1; i++) {
                int num1 = sequence[i];
                int num2 = sequence[i + 1];
                if (!graph.containsKey(num1)) {
                    graph.put(num1, new HashSet<>());
                    indegrees.put(num1, 0);
                }
                if (!graph.containsKey(num2)) {
                    graph.put(num2, new HashSet<>());
                    indegrees.put(num2, 0);
                }
                if (!graph.get(num1).contains(num2)) {
                    graph.get(num1).add(num2);
                    indegrees.put(num2, indegrees.get(num2) + 1);
                }
            }
        }

        if (nums.length != graph.size())
            return false;
        for (int num : nums)
            if (!graph.containsKey(num))
                return false;

        Queue<Integer> queue = new ArrayDeque<>();
        for (int num : indegrees.keySet())
            if (indegrees.get(num) == 0)
                queue.offer(num);

        int[] res = new int[nums.length];
        int index = 0;
        while (!queue.isEmpty()) {
            if (queue.size() != 1)
                return false;
            int num = queue.poll();
            res[index++] = num;
            for (int next : graph.get(num)) {
                indegrees.put(next, indegrees.get(next) - 1);
                if (indegrees.get(next) == 0)
                    queue.offer(next);
            }
        }
        if (index != nums.length)
            return false;
        for (int i = 0; i < index; i++)
            if (res[i] != nums[i])
                return false;

        return true;
    }

    // 剑指Offer Il 116.省份数量（主站547.）
    // 有 n 个城市，其中一些彼此相连，另一些没有相连。如果城市 a 与城市 b 直接相连，且城市 b 与城市 c 直接相连，那么城市 a 与城市 c
    // 间接相连。
    // 省份 是一组直接或间接相连的城市，组内不含其他没有相连的城市。
    // 给你一个 n x n 的矩阵 isConnected ，其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连，
    // 而 isConnected[i][j] = 0 表示二者不直接相连。
    // 返回矩阵中 省份 的数量。
    // 就是找连通分支数

    // 方法一：dfs-时间复杂度：O(n2)，空间复杂度：O(n)
    public int findCircleNum(int[][] isConnected) {
        int provinces = isConnected.length;
        boolean[] visited = new boolean[provinces];
        int circles = 0;
        for (int i = 0; i < provinces; i++)// 对每个节点尝试dfs
            if (!visited[i]) {
                dfs(isConnected, visited, provinces, i);
                circles++;
            }
        return circles;
    }

    public void dfs(int[][] isConnected, boolean[] visited, int provinces, int i) {
        for (int j = 0; j < provinces; j++)
            if (isConnected[i][j] == 1 && !visited[j]) {// 相连且未访问过
                visited[j] = true;
                dfs(isConnected, visited, provinces, j);
            }
    }

    // 方法二：bfs-时间复杂度：O(n2)，空间复杂度：O(n)
    public int findCircleNum2(int[][] isConnected) {
        int provinces = isConnected.length;
        boolean[] visited = new boolean[provinces];
        int circles = 0;
        Queue<Integer> queue = new LinkedList<Integer>();
        for (int i = 0; i < provinces; i++)// 对每个节点尝试bfs
            if (!visited[i]) {
                queue.offer(i);
                while (!queue.isEmpty()) {
                    int j = queue.poll();
                    visited[j] = true;
                    for (int k = 0; k < provinces; k++)
                        if (isConnected[j][k] == 1 && !visited[k])
                            queue.offer(k);
                }
                circles++;
            }
        return circles;
    }

    // 方法三：并查集-时间复杂度：O(n2logn)，空间复杂度：O(n)
    public int findCircleNum3(int[][] isConnected) {
        int provinces = isConnected.length;
        int[] parent = new int[provinces];
        for (int i = 0; i < provinces; i++)
            parent[i] = i;

        for (int i = 0; i < provinces; i++)
            for (int j = i + 1; j < provinces; j++)
                if (isConnected[i][j] == 1)
                    union(parent, i, j);

        int circles = 0;
        for (int i = 0; i < provinces; i++)
            if (parent[i] == i)
                circles++;

        return circles;
    }

    // 节点1的祖先指向节点2的祖先节点
    public void union(int[] parent, int index1, int index2) {
        parent[find(parent, index1)] = find(parent, index2);
    }

    // 找祖先节点
    public int find(int[] parent, int index) {
        if (parent[index] != index)
            parent[index] = find(parent, parent[index]);
        return parent[index];
    }

    // 剑指Offer II 117.相似的字符串（主站839.）
    // 如果交换字符串 X 中的两个不同位置的字母，使得它和字符串 Y 相等，那么称 X 和 Y 两个字符串相似。
    // 如果这两个字符串本身是相等的，那它们也是相似的。

    // 例如，"tars" 和 "rats" 是相似的 (交换 0 与 2 的位置)； 
    // "rats" 和 "arts" 也是相似的，但是 "star" 不与 "tars"，"rats"，或 "arts" 相似。
    // 总之，它们通过相似性形成了两个关联组：{"tars", "rats", "arts"} 和 {"star"}。
    // 注意，"tars" 和 "arts" 是在同一组中，即使它们并不相似。
    // 形式上，对每个组而言，要确定一个单词在组中，只需要这个词和该组中至少一个单词相似。
    // 给定一个字符串列表 strs。列表中的每个字符串都是 strs 中其它所有字符串的一个 字母异位词 。
    // 请问 strs 中有多少个相似字符串组？
    // 字母异位词（anagram）：一种把某个字符串的字母的位置（顺序）加以改换所形成的新词。
    // 提示：
    // 1 <= strs.length <= 300
    // 1 <= strs[i].length <= 300
    // strs[i] 只包含小写字母。
    // strs 中的「所有单词都具有相同的长度，且是彼此的字母异位词」。
    // 相似规则：交换字母位置

    // 处理图的边（判断是否为字母异位词）：不要真的通过交换字符位置来判断O(n2)，直接遍历字符判断O(n)

    // 方法一：并查集-时间复杂度：O(n2m+nlogn))，空间复杂度：O(n)
    // 对于每个节点，判断与其他节点是否合并，未合并则判断是否为字母异位词，是则合并（无需建图，效率更高一些）
    // dfs bfs也可，先确定边关系（建图），再dfs bfs确定连通分支数（也可同时进行，即一边遍历一边判边）
    int[] parent;

    public int numSimilarGroups(String[] strs) {
        int n = strs.length;// 单词数
        int m = strs[0].length();// 单词长度
        parent = new int[n];
        for (int i = 0; i < n; i++)
            parent[i] = i;

        for (int i = 0; i < n; i++)
            for (int j = i + 1; j < n; j++) {
                int fi = find(i), fj = find(j);
                if (fi == fj)
                    continue;

                if (check(strs[i], strs[j], m))
                    parent[fi] = fj;

            }

        int ret = 0;
        for (int i = 0; i < n; i++)
            if (parent[i] == i)
                ret++;

        return ret;
    }

    public int find(int x) {
        return parent[x] == x ? x : (parent[x] = find(parent[x]));
    }

    // 题目条件：「所有单词都具有相同的长度，且是彼此的字母异位词」
    // 字母异位词（anagram）：一种把某个字符串的字母的位置（顺序）加以改换所形成的新词。同位置字符不同的数量只能为0,2,4...
    // 个人感觉无法从该题目条件得到这个推论，但确实题中同位置字符不同的数量不会为1
    public boolean check(String a, String b, int len) {
        int num = 0;
        for (int i = 0; i < len; i++)
            if (a.charAt(i) != b.charAt(i)) {
                num++;
                if (num > 2)
                    return false;
            }
        return true;
    }

    // 方法二：（自己写的）dfs
    // 处理图的边（判断是否为字母异位词）：不要真的通过交换字符位置来判断O(n2)，直接遍历字符判断O(n)
    Map<String, Integer> strId = new HashMap<>();
    List<List<Integer>> graph117 = new ArrayList<>();
    boolean[] visited117;

    public int numSimilarGroups2(String[] strs) {
        int index = 0;
        for (String str : strs) {
            strId.put(str, index++);
            graph117.add(new ArrayList<>());
        }

        // 遍历节点，建图
        for (String str1 : strs)
            for (String str2 : strs)
                if (check(str1, str2))
                    graph117.get(strId.get(str1)).add(strId.get(str2));

        visited117 = new boolean[index];
        int res = 0;
        for (String str : strs)
            if (!visited117[strId.get(str)]) {
                res++;
                dfs117(strId.get(str));
            }

        return res;
    }

    private void dfs117(int id) {
        visited117[id] = true;
        for (int next : graph117.get(id))
            if (!visited117[next])
                dfs117(next);
    }

    // 判断两个字符串是否为字母异位词
    private boolean check(String str1, String str2) {
        if (str1.equals(str2)) // 不用自己连自己
            return false;
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < str1.length(); i++) {
            if (str1.charAt(i) != str2.charAt(i))
                list.add(i);
            if (list.size() > 2)
                return false;
        }
        if (list.size() != 2)
            return false;
        int index1 = list.get(0);
        int index2 = list.get(1);

        if (str1.charAt(index1) == str2.charAt(index2) && str1.charAt(index2) == str2.charAt(index1))
            return true;
        return false;
    }

    // 剑指Offer II 118.多余的边（主站684.）
    // 树可以看成是一个连通且 无环 的 无向 图。
    // 给定往一棵 n 个节点 (节点值 1～n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间，且这条附加的边不属于树中已存在的边。
    // 图的信息记录于长度为 n 的二维数组 edges ，edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。
    // 请找出一条可以删去的边，删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案，则返回数组 edges 中最后出现的边。

    // 由题意，只有并查集比较好做，答案跟边的顺序有关，先构建完整图再dfs bfs可能无法得到题意答案

    // 方法一：并查集-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public int[] findRedundantConnection(int[][] edges) {
        // 在一棵树中，边的数量比节点的数量少 1。如果一棵树有 n 个节点，则这棵树有 n-1 条边。
        // 这道题中的图在树的基础上多了一条附加的边，因此边的数量也是 n。
        int n = edges.length;// 边数就是节点数
        int[] parent = new int[n + 1];
        DSU118 dsu = new DSU118();
        for (int i = 1; i <= n; i++)
            parent[i] = i;

        for (int i = 0; i < n; i++) {
            int[] edge = edges[i];
            int node1 = edge[0], node2 = edge[1];
            if (dsu.find(parent, node1) != dsu.find(parent, node2))
                dsu.union(parent, node1, node2);
            else // 两节点祖先节点相同，则已在同一集合中，即成环
                return edge;

        }
        return new int[0];
    }

    class DSU118 {
        public void union(int[] parent, int index1, int index2) {
            parent[find(parent, index1)] = find(parent, index2);
        }

        public int find(int[] parent, int index) {
            if (parent[index] != index)
                parent[index] = find(parent, parent[index]);

            return parent[index];
        }
    }

    // 方法一：（自己写的）并查集-时间复杂度：O(nlogn)，空间复杂度：O(n)
    int[] fathers;

    public int[] findRedundantConnection11(int[][] edges) {
        // 先找到总节点数
        int num = 0;
        for (int[] edge : edges)
            num = Math.max(num, Math.max(edge[0], edge[1]));

        fathers = new int[num + 1];
        for (int i = 1; i <= num; i++)
            fathers[i] = i;

        int[] res = new int[2];
        for (int[] edge : edges) {
            int node1 = edge[0];
            int node2 = edge[1];
            // 如果有多个答案，则返回数组 edges 中最后出现的边。
            if (getFather(node1) == getFather(node2)) {
                res[0] = node1;
                res[1] = node2;
                continue;
            }
            fathers[getFather(node2)] = fathers[node1];
        }
        return res;
    }

    // 寻找父节点 + 路径压缩
    private int getFather(int node) {
        if (fathers[node] == node)
            return node;
        int father = getFather(fathers[node]);
        fathers[node] = father;
        return father;
    }
}

// 设计
class DesignOfferII {
    // 剑指Offer II 013.二维子矩阵的和（主站304.）
    // 这道题是「303. 区域和检索 - 数组不可变」的进阶，第 303 题是在一维数组中做区域和检索，这道题是在二维矩阵中做区域和检索。
    // 给定一个二维矩阵 matrix，以下类型的多个请求：
    // 计算其子矩形范围内元素的总和，该子矩阵的左上角为 (row1, col1) ，右下角为 (row2, col2) 。
    // 实现 NumMatrix 类：
    // NumMatrix(int[][] matrix) 给定整数矩阵 matrix 进行初始化
    // int sumRegion(int row1, int col1, int row2, int col2) 
    // 返回左上角 (row1, col1) 、右下角 (row2, col2) 的子矩阵的元素总和。

    // 方法一：一维前缀和-时空复杂度：O(mn)，每次检索的时间复杂度是 O(m)
    // 预处理：每一行累加
    // 初始化时对矩阵的每一行计算前缀和，检索时对二维区域中的每一行计算子数组和，然后对每一行的子数组和计算总和。
    class NumMatrix {
        int[][] sums;

        public NumMatrix(int[][] matrix) {
            int m = matrix.length;
            if (m > 0) {
                int n = matrix[0].length;

                // sums[i] 为 matrix[i] 的前缀和数组，计算区间[0, j)
                // 将 sums 的列数设为 n+1 的目的是为了方便计算每一行的子数组和，不需要对 j = 0 的情况特殊处理。
                sums = new int[m][n];

                for (int i = 0; i < m; i++)
                    for (int j = 0; j < n; j++)
                        sums[i][j + 1] = sums[i][j] + matrix[i][j];
            }
        }

        public int sumRegion(int row1, int col1, int row2, int col2) {
            int sum = 0;
            for (int i = row1; i <= row2; i++)
                sum += sums[i][col2 + 1] - sums[i][col1];

            return sum;
        }
    }

    // 方法二：二维前缀和-时空复杂度：O(mn)，每次检索的时间复杂度是 O(1)
    // 方法一虽然利用了前缀和，但是每次检索的时间复杂度是 O(m)，仍然没有降到 O(1)。
    // 预处理：左上角矩形累加
    // 为了将每次检索的时间复杂度降到 O(1)，需要使用二维前缀和，在初始化的时候计算二维前缀和数组。
    class NumMatrix2 {
        int[][] sums;

        public NumMatrix2(int[][] matrix) {
            int m = matrix.length;
            if (m > 0) {
                int n = matrix[0].length;
                // 方便处理，计算区间[0, i) [0, j)
                sums = new int[m + 1][n + 1];
                for (int i = 0; i < m; i++)
                    for (int j = 0; j < n; j++)
                        // 上半边矩形 + 左半边矩形 - 左上角矩形 + 自己
                        sums[i + 1][j + 1] = sums[i][j + 1] + sums[i + 1][j] - sums[i][j] + matrix[i][j];

            }
        }

        public int sumRegion(int row1, int col1, int row2, int col2) {
            // 完整矩形 - 上半边矩形 - 左半边矩形 + 左上角矩形
            return sums[row2 + 1][col2 + 1] - sums[row1][col2 + 1] - sums[row2 + 1][col1] + sums[row1][col1];
        }
    }

    // 剑指Offer II 030.插入、删除和随机访问都是 O(1) 的容器（主站380.）
    // 设计一个支持在平均 时间复杂度 O(1) 下，执行以下操作的数据结构：
    // insert(val)：当元素 val 不存在时返回 true ，并向集合中插入该项，否则返回 false 。
    // remove(val)：当元素 val 存在时返回 true ，并从集合中移除该项，否则返回 false 。
    // getRandom：随机返回现有集合中的一项。每个元素应该有 相同的概率 被返回。

    // 方法一：哈希表 + 动态数组
    // 哈希表无法随机存取，列表无法在O(1)时间内插入删除，各取长处。

    // 虽然哈希表提供O(1)时间的插入和删除，但是实现 getRandom 时会出现问题。
    // getRandom 的思想是选择一个随机索引，然后使用该索引返回一个元素。
    // 而哈希表中没有索引，因此要获得真正的随机值，则要将哈希表中的键转换为列表，这需要线性时间。
    // 解决的方法是用一个列表存储值，并在该列表中实现常数时间的 getRandom。
    class RandomizedSet {
        // 利哈希表的常数时间的插入和删除，维护元素值及其在list中对应的索引值
        // 插入重复数字返回失败
        Map<Integer, Integer> map;
        // 利用列表的随机存取，自动扩容，其他使用与数组一样（删除中间元素list会自动移动后续元素，不会这样删除）
        List<Integer> list;
        Random rand = new Random();

        public RandomizedSet() {
            map = new HashMap<>();
            list = new ArrayList<>();
        }

        public boolean insert(int val) {
            if (map.containsKey(val))
                return false;

            map.put(val, list.size());
            list.add(list.size(), val);
            return true;
        }

        public boolean remove(int val) {
            if (!map.containsKey(val))
                return false;

            int lastElement = list.get(list.size() - 1);// 从list获取末尾元素值
            int idx = map.get(val);// 通过map获取要删除元素的索引
            list.set(idx, lastElement);// 将list中要删除元素替换为末尾元素
            map.put(lastElement, idx);// 更新map中末尾元素的索引值
            list.remove(list.size() - 1);// 删除list中末尾元素（已移至被删除元素位置）
            map.remove(val);// 删除map中对应元素，map删除一定要放在put之后，如果删除元素刚好是列表中最后一个，可以不用特殊处理
            return true;
        }

        public int getRandom() {
            return list.get(rand.nextInt(list.size()));
        }
    }

    // 剑指Offer II 031.最近最少使用缓存（主站146.hot100有）
    // 运用所掌握的数据结构，设计和实现一个  LRU (Least Recently Used，最近最少使用) 缓存机制 。
    // 实现 LRUCache 类：
    // LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存
    // int get(int key) 如果关键字 key 存在于缓存中，则返回关键字的值，否则返回 -1 。
    // void put(int key, int value) 如果关键字已经存在，则变更其数据值；如果关键字不存在，则插入该组「关键字-值」。
    // 当缓存容量达到上限时，它应该在写入新数据之前删除最久未使用的数据值，从而为新的数据值留出空间。

    // 方法一：哈希表 + 双向链表-时间复杂度：O(1)，空间复杂度：O(capacity)
    // 利用哈希表实现O(1)的查找
    // 利用双向链表实现O(1)的删除、插入
    // 在面试中这种设计题，面试官一般会期望读者能够自己实现一个简单的双向链表，而不是使用语言自带的、封装好的数据结构。
    // 在双向链表的实现中，使用一个伪头部（dummy head）和伪尾部（dummy tail）标记界限，
    // 这样在添加节点和删除节点的时候就不需要检查相邻的节点是否存在。
    class LRUCache {
        class DLinkedNode {// 双向链表节点
            int key;
            int value;
            DLinkedNode prev;
            DLinkedNode next;

            public DLinkedNode() {
            }

            public DLinkedNode(int _key, int _value) {
                key = _key;
                value = _value;
            }
        }

        private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();// 利用哈希表实现O(1)的查找
        private DLinkedNode dummyHead, dummyTail;// 利用双向链表实现O(1)的删除、插入
        private int size;// 当前缓存内个数
        private int capacity;// 最大容量，初始化时定义

        public LRUCache(int capacity) {
            this.size = 0;
            this.capacity = capacity;
            // 使用伪头部和伪尾部节点
            dummyHead = new DLinkedNode();
            dummyTail = new DLinkedNode();
            dummyHead.next = dummyTail;
            dummyTail.prev = dummyHead;
        }

        public int get(int key) {
            DLinkedNode node = cache.get(key);
            if (node == null)
                return -1;

            // 如果 key 存在，先通过哈希表定位，再移到头部
            moveToHead(node);
            return node.value;
        }

        public void put(int key, int value) {
            DLinkedNode node = cache.get(key);
            if (node == null) {// 如果 key 不存在
                DLinkedNode newNode = new DLinkedNode(key, value);// 创建一个新的节点
                cache.put(key, newNode); // 添加进哈希表
                addToHead(newNode); // 添加至双向链表的头部
                ++size;
                if (size > capacity) {
                    DLinkedNode tail = removeTail();// 如果超出容量，删除双向链表的尾部节点
                    cache.remove(tail.key);// 删除哈希表中对应的项
                    --size;
                }
            } else {
                // 如果 key 存在，先通过哈希表定位，再修改 value，并移到头部
                node.value = value;
                moveToHead(node);
            }
        }

        private void addToHead(DLinkedNode node) {
            DLinkedNode prevHead = dummyHead.next;
            node.prev = dummyHead;
            node.next = prevHead;
            prevHead.prev = node;
            dummyHead.next = node;
        }

        private void removeNode(DLinkedNode node) {
            DLinkedNode prevNode = node.prev;
            DLinkedNode nextNode = node.next;
            prevNode.next = nextNode;
            nextNode.prev = prevNode;
        }

        private void moveToHead(DLinkedNode node) {
            removeNode(node);
            addToHead(node);
        }

        private DLinkedNode removeTail() {
            DLinkedNode res = dummyTail.prev;
            removeNode(res);
            return res;
        }
    }

    // 方法一：（自己写的）哈希表 + 双向链表-时间复杂度：O(1)，空间复杂度：O(capacity)
    // 注意 哈希表 和 双向链表 的操作一致性
    class DLinkNode {
        int key;
        int val;
        DLinkNode prev;
        DLinkNode next;

        DLinkNode(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }

    class LRUCache11 {
        int capacity;
        Map<Integer, DLinkNode> keyNode = new HashMap<>();
        DLinkNode dummyHead = new DLinkNode(-1, -1);
        DLinkNode dummyTail = new DLinkNode(-1, -1);

        public LRUCache11(int capacity) {
            this.capacity = capacity;
            dummyHead.next = dummyTail;
            dummyTail.prev = dummyHead;
        }

        public int get(int key) {
            if (keyNode.containsKey(key)) {
                DLinkNode node = keyNode.get(key);
                delete(node);
                insert(node, dummyTail);
                return node.val;
            } else
                return -1;

        }

        public void put(int key, int value) {
            if (keyNode.containsKey(key)) {// 已有该key，更新节点的val，更新在链表中的位置
                DLinkNode node = keyNode.get(key);
                node.val = value;
                delete(node);
                insert(node, dummyTail);

            } else if (capacity == keyNode.size()) {// 未有该key且cache已满，队头节点出队，新节点加入队尾
                DLinkNode removeNode = dummyHead.next;
                delete(removeNode);
                keyNode.remove(removeNode.key);

                DLinkNode node = new DLinkNode(key, value);
                insert(node, dummyTail);
                keyNode.put(key, node);
            } else {// 未有该key，且cache未满，新节点加入队尾
                DLinkNode node = new DLinkNode(key, value);
                insert(node, dummyTail);
                keyNode.put(key, node);
            }

        }

        private void delete(DLinkNode node) {
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }

        private void insert(DLinkNode node, DLinkNode next) {
            DLinkNode prev = next.prev;
            prev.next = node;
            next.prev = node;

            node.next = next;
            node.prev = prev;
        }
    }

    // 方法二：自己写的lowb，用java自带的LinkedHashMap-时间复杂度：O(1)，空间复杂度：O(capacity)
    // LinkedHashMap 放入哈希表时会用链表记录放入的先后顺序
    class LRUCache2 {
        Map<Integer, Integer> cache;
        int capacity;

        public LRUCache2(int capacity) {
            this.capacity = capacity;
            cache = new LinkedHashMap<>();
        }

        public int get(int key) {
            if (cache.containsKey(key)) {
                Integer value = cache.get(key);
                // 删除再放入，改变内置链表的先后顺序
                cache.remove(key);
                cache.put(key, value);
                return value;
            } else
                return -1;
        }

        public void put(int key, int value) {
            if (cache.containsKey(key))
                cache.remove(key);
            else if (cache.size() == capacity) {// 去除缓存中最先被放入的元素
                Set<Integer> keySet = cache.keySet();
                Iterator<Integer> keyIterator = keySet.iterator();
                cache.remove(keyIterator.next()); // 一定不用判断hasNext()
            }
            cache.put(key, value);
        }
    }

    // 剑指Offer II 041.滑动窗口的平均值（主站346.）
    // 给定一个整数数据流和一个窗口大小，根据该滑动窗口的大小，计算滑动窗口里所有数字的平均值。
    // 实现 MovingAverage 类：
    // MovingAverage(int size) 用窗口大小 size 初始化对象。
    // double next(int val) 成员函数 next 每次调用的时候都会往滑动窗口增加一个整数，
    // 请计算并返回数据流中最后 size 个值的移动平均值，即滑动窗口里所有数字的平均值。

    // 方法一：队列-时间复杂度：O(n)，空间复杂度：O(m)，m = 窗口大小
    class MovingAverage {
        private int length;
        private Queue<Integer> queue;
        private double sum = 0;

        public MovingAverage(int size) {
            length = size;
            queue = new LinkedList<>();
            sum = 0;
        }

        public double next(int val) {
            if (queue.size() == length)
                sum -= queue.poll();

            queue.offer(val);
            sum += val;
            return sum / queue.size();
        }
    }

    // 剑指Offer II 042.最近请求次（主站933.）
    // 写一个 RecentCounter 类来计算特定时间范围内最近的请求。
    // 请实现 RecentCounter 类：
    // RecentCounter() 初始化计数器，请求数为 0 。
    // int ping(int t) 在时间 t 添加一个新请求，其中 t 表示以毫秒为单位的某个时间，
    // 并返回过去 3000 毫秒内发生的所有请求数（包括新请求）。 确切地说，返回在 [t-3000, t] 内发生的请求数。
    // 保证 每次对 ping 的调用都使用比之前更大的 t 值。

    // 方法一：队列-时间复杂度：O(1)，空间复杂度：O(n)
    class RecentCounter {
        Queue<Integer> q;

        public RecentCounter() {
            q = new LinkedList<>();
        }

        public int ping(int t) {
            q.offer(t);
            while (q.peek() < t - 3000)
                q.poll();
            return q.size();
        }
    }

    // 剑指Offer II 043.往完全二叉树...
    // 剑指Offer II 048.序列化与反...
    // 剑指Offer II 055.二叉搜索树迭..

    // 剑指Offer II 058.日程表（主站729.）
    // 请实现一个 MyCalendar 类来存放你的日程安排。如果要添加的时间内没有其他安排，则可以存储这个新的日程安排。
    // MyCalendar 有一个 book(int start, int end)方法。它意味着在 start 到 end 时间内增加一个日程安排，
    // 注意，这里的时间是半开区间，即 [start, end), 实数 x 的范围为，  start <= x < end。
    // 当两个日程安排有一些时间上的交叉时（例如两个日程安排都在同一时间内），就会产生重复预订。
    // 每次调用 MyCalendar.book方法时，如果可以将日程安排成功添加到日历中而不会导致重复预订，返回 true。
    // 否则，返回 false 并且不要将该日程安排添加到日历中。
    // 请按照以下步骤调用 MyCalendar 类:
    // MyCalendar cal = new MyCalendar();
    // MyCalendar.book(start, end)

    // 方法一：平衡树 TreeMap -时间复杂度：O(NlogN)，空间复杂度：O(N)
    class MyCalendar {
        TreeMap<Integer, Integer> calendar;

        MyCalendar() {
            calendar = new TreeMap<>();
        }

        public boolean book(int start, int end) {
            Integer prev = calendar.floorKey(start), // 前一日程开始时间（小于等于）
                    next = calendar.ceilingKey(start);// 后一日程开始时间（大于等于）
            // ****************** 前一日程结束时间
            if ((prev == null || calendar.get(prev) <= start) && (next == null || end <= next)) {
                calendar.put(start, end);
                return true;
            }
            return false;
        }
    }

    // 方法一：（自己写的）平衡树 TreeMap -时间复杂度：O(NlogN)，空间复杂度：O(N)
    class MyCalendar11 {
        TreeMap<Integer, Integer> mapStartEnd;

        public MyCalendar11() {
            mapStartEnd = new TreeMap<>();
        }

        public boolean book(int start, int end) {
            if (mapStartEnd.containsKey(start))
                return false;

            Integer prev = mapStartEnd.lowerKey(start);
            Integer next = mapStartEnd.higherKey(start);

            if (prev != null && mapStartEnd.get(prev) > start)
                return false;
            if (next != null && next < end)
                return false;

            mapStartEnd.put(start, end);
            return true;
        }
    }

    // 剑指Offer II 059.数据流的第K .. .

    // 剑指Offer II 062.实现前缀树（主站208. hot100有）
    // Trie（发音类似 "try"）或者说 前缀树 是一种树形数据结构，用于高效地存储和检索字符串数据集中的键。
    // 这一数据结构有相当多的应用情景，例如自动补完和拼写检查。
    // 请你实现 Trie 类：
    // Trie() 初始化前缀树对象。
    // void insert(String word) 向前缀树中插入字符串 word 。
    // boolean search(String word) 如果字符串 word 在前缀树中，返回 true（即，在检索之前已经插入）；
    // 否则，返回 false 。
    // boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix ，返回 true ；
    // 否则，返回 false 。

    // 方法一：字典树-时间复杂度：初始化为O(1)，其余操作为 O(|S|)，其中 |S| 是每次插入或查询的字符串的长度。
    // 空间复杂度：O(|T|⋅Σ)，其中 |T| 为所有插入字符串的长度之和，Σ 为字符集的大小，本题 Σ=26。
    // Trie，又称前缀树或字典树，是一棵有根树，其每个节点包含以下字段：
    // 指向子节点的指针数组 children。对于本题而言，数组长度为 26，即小写英文字母的数量。
    // 此时 children[0] 对应小写字母 a，children[1] 对应小写字母 b，…，children[25] 对应小写字母 z。
    // 布尔字段 isEnd，表示该节点是否为字符串的结尾。
    // （本题可看成一棵26叉树，以边表示字母）
    class Trie {
        TrieNode headNode;

        Trie() {
            headNode = new TrieNode();
        }

        private class TrieNode {
            TrieNode[] children;
            boolean isEnd; // 默认false

            TrieNode() {
                children = new TrieNode[26];
            }
        }

        public void insert(String word) {
            TrieNode node = headNode;// node指向头节点
            for (int i = 0; i < word.length(); i++) {// 逐个遍历每个字符
                char ch = word.charAt(i);
                int index = ch - 'a'; // 从a开始索引值为0
                if (node.children[index] == null) // 若无则创建节点
                    node.children[index] = new TrieNode();

                node = node.children[index];// 指向当前字符
            }
            node.isEnd = true;// 表示字符串结尾
        }

        public boolean search(String word) {
            TrieNode node = searchPrefix(word);
            return node != null && node.isEnd;// 存在该字符串，且为字符串结尾
        }

        public boolean startsWith(String prefix) {
            return searchPrefix(prefix) != null;// 无需为字符串结尾
        }

        // 查询前缀，返回末尾字符节点
        private TrieNode searchPrefix(String prefix) {
            TrieNode node = headNode;
            for (int i = 0; i < prefix.length(); i++) {
                char ch = prefix.charAt(i);
                int index = ch - 'a';
                if (node.children[index] == null)
                    return null;

                node = node.children[index];
            }
            return node;
        }
    }

    // 方法一：（自己写的前缀树，没必要搞个内部类）
    class Trie11 {
        Trie11[] vals;
        boolean isEnd;

        public Trie11() {
            vals = new Trie11[26];
            isEnd = false;
        }

        public void insert(String word) {
            Trie11 curr = this;
            for (char c : word.toCharArray()) {
                int index = c - 'a';
                if (curr.vals[index] == null)
                    curr.vals[index] = new Trie11();
                curr = curr.vals[index];
            }
            curr.isEnd = true;
        }

        public boolean search(String word) {
            Trie11 curr = this;
            for (char c : word.toCharArray()) {
                int index = c - 'a';
                if (curr.vals[index] == null)
                    return false;
                curr = curr.vals[index];
            }
            return curr.isEnd;
        }

        public boolean startsWith(String prefix) {
            Trie11 curr = this;
            for (char c : prefix.toCharArray()) {
                int index = c - 'a';
                if (curr.vals[index] == null)
                    return false;
                curr = curr.vals[index];
            }
            return true;
        }
    }

    // 方法二：有序集合-时间复杂度：初始化为O(1)，其余操作为O(log|S|)
    // 空间复杂度：O(n)，字符串的个数
    // （自己的lowb想法：二叉排序树（java的TreeSet），查找前缀：先找插入位置，再比较父节点）
    // 在时间复杂度上，前缀树解法明显优于有序集合解法。
    // 其原因在于：使用前缀树（Trie）的数据结构使得插入、查询全词、查询前缀的时间复杂度与已插入的单词数目无关，
    // 这是前缀树（Trie）解法优于有序集合解法的关键
    class Trie2 {
        private TreeSet<String> treeSet;

        Trie2() {
            treeSet = new TreeSet<>();
        }

        public void insert(String word) {
            treeSet.add(word);
        }

        public boolean search(String word) {
            return treeSet.contains(word);
        }

        public boolean startsWith(String prefix) {
            if (treeSet.contains(prefix))
                return true;

            // 前缀可能包含prefix的字符串一定大于prefix（字符串的比较）
            String high = treeSet.higher(prefix);
            if (high != null && high.startsWith(prefix))
                return true;
            return false;
        }
    }

    // 剑指Offer II 064.神奇的字典（主站676.）
    // 设计一个使用单词列表进行初始化的数据结构，单词列表中的单词 互不相同 。
    // 如果给出一个单词，请判定能否只将这个单词中一个字母换成另一个字母，使得所形成的新单词存在于已构建的神奇字典中。
    // 实现 MagicDictionary 类：
    // MagicDictionary() 初始化对象
    // void buildDict(String[] dictionary)
    // 使用字符串数组 dictionary 设定该数据结构，dictionary中的字符串互不相同
    // bool search(String searchWord)
    // 给定一个字符串 searchWord ，判定能否只将字符串中 一个 字母换成另一个字母，使得所形成的新字符串能够与字典中的任一字符串匹配。
    // 如果可以，返回 true ；否则，返回 false 。
    // 提示：
    // 1 <= dictionary.length <= 100
    // 1 <= dictionary[i].length <= 100
    // dictionary[i] 仅由小写英文字母组成
    // dictionary 中的所有字符串 互不相同
    // 1 <= searchWord.length <= 100
    // searchWord 仅由小写英文字母组成
    // buildDict 仅在 search 之前调用一次
    // 最多调用 100 次 search

    // 方法一：哈希表 <word.length, List<String>>
    // 时间复杂度：O(N) 构建，O(NK) 搜索，空间复杂度：O(S)
    // 其中 N 是魔法字典中的单词数，S 是其中的字母总数，K 是搜索单词的长度。
    class MagicDictionary {
        Map<Integer, List<String>> buckets;

        public MagicDictionary() {
            buckets = new HashMap<>();
        }

        public void buildDict(String[] words) {
            for (String word : words) {
                // 如果指定的键尚未与值关联（或映射为null），
                // 则尝试使用给定的mapping函数计算其值，并将其输入此mapping函数，键与此输出关联，返回键值
                // 如果指定的键有值，则返回该值，即List<String>
                // 实现的效果：初次遇到某长度的字符串时，创建该键下的List并将该字符串加入List，后续遇到则直接将字符串加入List
                buckets.computeIfAbsent(word.length(), x -> new ArrayList<>()).add(word);
                // 等价于
                // int key = word.length();
                // List<String> words = map.getOrDefault(key, new ArrayList<String>());
                // words.add(word);
                // map.put(key, words);
            }
        }

        public boolean search(String word) {
            if (!buckets.containsKey(word.length()))// 没有长度相等的字符串，则一定没有答案
                return false;
            for (String candidate : buckets.get(word.length())) {// 逐个计算每个字符是否相同
                int mismatch = 0;// 不相等字符数
                for (int i = 0; i < word.length(); ++i)
                    if (word.charAt(i) != candidate.charAt(i))
                        if (++mismatch > 1)
                            break;

                if (mismatch == 1)// 只有一个不等的字符才是正确答案
                    return true;
            }
            return false;
        }
    }

    // 方法二：广义邻居（实际时间差蛮多的，主要是构造时花费时间多）
    // 时间复杂度：O(S) 构建，O(K) 搜索，空间复杂度：O(S)
    // 其中 N 是魔法字典中的单词数，S 是其中的字母总数，K 是搜索单词的长度。

    // 如果一个单词中只有一个字符可以更改以使字符串相等，那么两个单词就是邻居。
    // 让我们假设一个词 “apple” 具有广义邻居 “pple”、“aple”、“aple”、“appe” 和 “appl”。
    // 在搜索像 apply 这样的词是否有像 apple 这样的邻居时，我们只需要知道它们是否有一个广义邻居。
    public class MagicDictionary2 {
        Set<String> words;// 存放所有字符串

        // 精髓之一，不需要实际表示连接关系，判断是否有邻居，仅需要通过邻居数
        Map<String, Integer> count;// <广义邻居（长度减一的子串，变化字符的位置用*替换），邻居数（字符串）>

        public MagicDictionary2() {
            words = new HashSet<>();
            count = new HashMap<>();
        }

        // 获取字符串的所有广义邻居（长度减一的子串，实际处理时将某位置为*）
        private ArrayList<String> generalizedNeighbors(String word) {
            ArrayList<String> ans = new ArrayList<>();
            char[] ca = word.toCharArray();
            for (int i = 0; i < word.length(); ++i) {
                char letter = ca[i];
                ca[i] = '*';
                String neighber = new String(ca);
                ans.add(neighber);
                ca[i] = letter;// 添加完后还原
            }
            return ans;
        }

        public void buildDict(String[] words) {
            for (String word : words) {
                this.words.add(word);
                for (String neighber : generalizedNeighbors(word)) // 遍历该词的所有广义邻居，使其邻居数+1
                    count.put(neighber, count.getOrDefault(neighber, 0) + 1);
            }
        }

        public boolean search(String word) {
            for (String neighber : generalizedNeighbors(word)) {// 遍历该词的所有广义邻居，获取其邻居数
                int c = count.getOrDefault(neighber, 0);

                // 精髓之一：该字符串的某个广义邻居的邻居数>1，或者邻居数=1且该字符串不存在于构建的字典中
                if (c > 1 || c == 1 && !words.contains(word))
                    return true;
            }
            return false;
        }
    }

    // 方法三：前缀树，search函数使用递归查找可能存在的答案，略

    // 剑指Offer II 066.单词之和（主站677.）
    // 实现一个 MapSum 类，支持两个方法，insert 和 sum：
    // MapSum() 初始化 MapSum 对象
    // void insert(String key, int val) 插入 key-val 键值对，字符串表示键 key ，整数表示值 val 。
    // 如果键 key 已经存在，那么原来的键值对将被替代成新的键值对。
    // int sum(string prefix) 返回所有以该前缀 prefix 开头的键 key 的值的总和。
    // 几种方法实际时间都差不多...

    // 方法一：暴力扫描（插入轻松，求和困难）
    // 时间复杂度：
    // insert 操作时间复杂度为 O(1)。
    // sum 操作时间复杂度为 O(NM)，其中 N 是插入的 key 的数目，M 是给定前缀 prefix 的长度。
    // 空间复杂度：O(NM)，其中 N 是插入的 key 的数目，M 是字符串 key 的最大长度。
    // 思路与算法
    // 我们将所有的 key-val 键值进行存储，每次需要搜索给定的前缀 prefix 时，我们依次搜索所有的键值。
    // 如果键值包含给定的前缀，则我们将其 val 进行相加，返回所有符合要求的 val 的和。
    class MapSum {
        Map<String, Integer> map;

        public MapSum() {
            map = new HashMap<>();
        }

        public void insert(String key, int val) {
            map.put(key, val);
        }

        public int sum(String prefix) {
            int res = 0;
            for (String s : map.keySet())
                if (s.startsWith(prefix))
                    res += map.get(s);

            return res;
        }
    }

    // 方法二：前缀哈希映射（插入困难，求和轻松）
    // 时间复杂度：
    // insert 操作时间复杂度为 O(N^2)，其中 N 是插入的字符串 key 的长度，我们需要把字符串的所有前缀都在哈希表中插入一次。
    // sum 操作时间复杂度为 O(1)
    // 空间复杂度：O(MN)，其中 M 表示 key-val 键值的数目，N 表示字符串 key 的最大长度
    class MapSum2 {
        Map<String, Integer> map;// 所有键，动态构建prefixmap时，判断是否出现重复键值对
        Map<String, Integer> prefixmap;// 所有前缀

        public MapSum2() {
            map = new HashMap<>();
            prefixmap = new HashMap<>();
        }

        public void insert(String key, int val) {
            // 如果键 key 已经存在，那么原来的键值对将被替代成新的键值对。
            int delta = val - map.getOrDefault(key, 0);// 变化值
            map.put(key, val);// 更新键值
            for (int i = 1; i <= key.length(); ++i) {// 枚举所有前缀，更新前缀和
                String currprefix = key.substring(0, i);// 前开后闭 [0, i)
                prefixmap.put(currprefix, prefixmap.getOrDefault(currprefix, 0) + delta);
            }
        }

        public int sum(String prefix) {
            return prefixmap.getOrDefault(prefix, 0);
        }
    }

    // 方法三：字典树（前缀树）（插入轻松，求和轻松）
    // 时间复杂度：insert 操作时间复杂度为O(N)，
    // 其中 N 是插入的字符串 key 的长度。sum 操作时间复杂度为 O(N)，其中 O(N) 为给定的查询字符的长度，需要在前缀树中搜索给定的前缀。
    // 空间复杂度：O(CNM)，其中 M 表示 key-val 键值的数目，N 表示字符串 key 的最大长度，C 为常数。

    // 由于我们要处理前缀，自然而然联想到可以用 Trie（前缀树）来处理此问题。
    // 处理方法跟方法二的原理一样，我们直接在前缀对应的 Trie 的每个节点存储该前缀对应的值。
    // insert 操作：原理与方法二一样，我们首先求出前缀对应的值的改变 delta，我们直接在 Trie 节点上更新键 key 的每个前缀对应的值。
    // sum 操作: 我们直接在前缀树上搜索该给定的前缀对应的值即可，如果给定的前缀不在前缀树中，则返回 0。
    // 当然在实际中我们也可以在 Trie 的节点只存储键 key 对应的 val, 每次求 sum 时利用 DFS 或者 BFS 遍历前缀树的子树即可。
    class MapSum3 {
        class TrieNode {// 前缀树，可以理解为边表示字符，节点表示前缀和
            int val = 0;// 前缀和
            TrieNode[] next = new TrieNode[26];
        }

        TrieNode root;// 前缀树根节点
        Map<String, Integer> map;// 存放所有键，动态构建前缀树时，判断是否出现重复键值对

        public MapSum3() {
            root = new TrieNode();
            map = new HashMap<>();
        }

        public void insert(String key, int val) {
            // 如果键 key 已经存在，那么原来的键值对将被替代成新的键值对。
            int delta = val - map.getOrDefault(key, 0);// 获取变化值
            map.put(key, val);// 更新键值
            TrieNode node = root;
            for (char c : key.toCharArray()) {// 更新前缀树
                if (node.next[c - 'a'] == null)// 无则创建
                    node.next[c - 'a'] = new TrieNode();

                node = node.next[c - 'a'];
                node.val += delta;
            }
        }

        public int sum(String prefix) {
            TrieNode node = root;
            for (char c : prefix.toCharArray()) {
                if (node.next[c - 'a'] == null) // 没有该前缀则返回0
                    return 0;
                node = node.next[c - 'a'];
            }
            return node.val;
        }
    }

    // 方法四：（自己写的）有序哈希 TreeMap
    // 时间复杂度：
    // insert 操作时间复杂度为 O(1)。
    // sum 操作时间复杂度为 O(logN)，其中 N 是插入的 key 的数目。
    // 空间复杂度：O(NM)，其中 N 是插入的 key 的数目，M 是字符串 key 的最大长度。
    class MapSum4 {
        TreeMap<String, Integer> map = new TreeMap<>();

        public MapSum4() {
        }

        public void insert(String key, int val) {
            map.put(key, val);
        }

        public int sum(String prefix) {
            int res = 0;
            String word = map.ceilingKey(prefix);
            while (word != null) {
                if (word.startsWith(prefix)) {
                    res += map.get(word);
                    word = map.higherKey(word);
                } else
                    break;
            }
            return res;
        }
    }
}

// 字典树
class DictionaryTreeOfferII {
    // 剑指Offer II 062.实现前缀树

    // 剑指Offer II 063.替换单词（主站648.）
    // 在英语中，有一个叫做 词根(root) 的概念，它可以跟着其他一些词组成另一个较长的单词——我们称这个词为 继承词(successor)。
    // 例如，词根an，跟随着单词 other(其他)，可以形成新的单词 another(另一个)。
    // 现在，给定一个由许多词根组成的词典和一个句子，需要将句子中的所有继承词用词根替换掉。如果继承词有许多可以形成它的词根，则用最短的词根替换它。
    // 需要输出替换之后的句子。
    // 提示：
    // 1 <= dictionary.length <= 1000
    // 1 <= dictionary[i].length <= 100
    // dictionary[i] 仅由小写字母组成。
    // 1 <= sentence.length <= 10^6
    // sentence 仅由小写字母和空格组成。
    // sentence 中单词的总量在范围 [1, 1000] 内。
    // sentence 中每个单词的长度在范围 [1, 1000] 内。
    // sentence 中单词之间由一个空格隔开。
    // sentence 没有前导或尾随空格。

    // 方法一：前缀树-时间复杂度：O(n)，空间复杂度：O(n)
    class TrieNode {
        TrieNode[] children;
        String word;// （针对改题的优化）是否是末尾节点，若是，则有值（root），若不是，则为null

        TrieNode() {
            children = new TrieNode[26];
        }
    }

    public String replaceWords(List<String> roots, String sentence) {
        TrieNode trie = new TrieNode();// 字典树头节点
        for (String root : roots) {// 预处理词根，生成字典树
            TrieNode cur = trie;
            for (char letter : root.toCharArray()) {
                if (cur.children[letter - 'a'] == null)// 无则创建
                    cur.children[letter - 'a'] = new TrieNode();
                cur = cur.children[letter - 'a'];
            }
            cur.word = root;// 标记末尾节点
        }

        // 组合出最终答案
        StringBuilder ans = new StringBuilder();// sb效率高
        for (String word : sentence.split(" ")) {
            if (ans.length() > 0)
                ans.append(" ");

            TrieNode cur = trie;
            for (char letter : word.toCharArray()) {
                if (cur.children[letter - 'a'] == null || cur.word != null)// 找不到前缀，或者已到末尾标记处
                    break;
                cur = cur.children[letter - 'a'];
            }
            ans.append(cur.word != null ? cur.word : word);// 若不是在末尾标记处停下，则保留整个word
        }
        return ans.toString();
    }

    // 方法一：（自己写的）前缀树-时间复杂度：O(n)，空间复杂度：O(n)
    class Trie {
        Trie[] vals = new Trie[26];
        boolean isEnd = false;

        Trie() {
        }
    }

    public String replaceWords11(List<String> dictionary, String sentence) {
        Trie root = new Trie();
        for (String word : dictionary) {
            Trie curr = root;
            for (char c : word.toCharArray()) {
                int index = c - 'a';
                if (curr.vals[index] == null)
                    curr.vals[index] = new Trie();
                curr = curr.vals[index];
            }
            curr.isEnd = true;
        }
        String[] strs = sentence.split(" ");
        for (int i = 0; i < strs.length; i++) {
            Trie curr = root;
            int count = 0;
            // 注意逻辑的先后顺序
            for (char c : strs[i].toCharArray()) {
                // 判断是否是词根
                if (curr.isEnd) {
                    strs[i] = strs[i].substring(0, count);
                    break;
                }
                // 判断退出或者进入下一层
                int index = c - 'a';
                if (curr.vals[index] == null)
                    break;
                curr = curr.vals[index];
                count++;
            }
        }

        StringBuilder res = new StringBuilder(strs[0]);
        for (int i = 1; i < strs.length; i++) {
            res.append(" ");
            res.append(strs[i]);
        }
        return res.toString();
    }

    // 方法二：（自己写的）有序集合 TreeSet-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 每次替换要查找treeset，logn的查询复杂度
    public String replaceWords2(List<String> dictionary, String sentence) {
        TreeSet<String> treeset = new TreeSet<>(dictionary);
        String[] words = sentence.split(" ");
        for (int i = 0; i < words.length; i++) {
            String word = words[i];
            String prev = treeset.lower(word);
            while (prev != null) {
                if (word.startsWith(prev))
                    words[i] = prev;
                prev = treeset.lower(prev);
            }
        }

        StringBuilder res = new StringBuilder(words[0]);
        for (int i = 1; i < words.length; i++) {
            res.append(" ");
            res.append(words[i]);
        }
        return res.toString();

    }

    // 剑指Offer II 064.神奇的字典

    // 剑指Offer II 065.最短的单词编码
    // 单词数组 words 的 有效编码 由任意助记字符串 s 和下标数组 indices 组成，且满足：
    // words.length == indices.length
    // 助记字符串 s 以 '#' 字符结尾
    // 对于每个下标 indices[i] ，s 的一个从 indices[i] 开始、到下一个 '#' 字符结束（但不包括 '#'）的 子字符串 恰好与
    // words[i] 相等
    // 给定一个单词数组 words ，返回成功对 words 进行编码的最小助记字符串 s 的长度 。

    // 方法一：（后缀）字典树 + 哈希-时间复杂度：O(mn)，空间复杂度：O(s)
    // n为词数，m为每个词的长度，s为总字符数

    // 设计字典树的节点时，把 isEnd（默认false）改为 hasNext（默认false），方便统计：以该节点结束的字符串，是否可以是其他字符串的子串
    // 硬要用 isEnd 也行（初始化为true即可，为了符合个人习惯）
    // 哈希表存储：<结束节点， 单词长度>，方便最后统计最终结果
    class TrieNode65 {
        TrieNode65[] children;
        boolean hasNext;

        TrieNode65() {
            children = new TrieNode65[26];
            hasNext = false;// 默认是最末尾的节点
        }
    }

    public int minimumLengthEncoding(String[] words) {
        TrieNode65 trie = new TrieNode65();// 字典树头节点
        Map<TrieNode65, Integer> nodes = new HashMap<TrieNode65, Integer>();// <结束节点， 单词长度>

        for (int i = 0; i < words.length; ++i) {// 建立字典树
            String word = words[i];
            TrieNode65 cur = trie;
            for (int j = word.length() - 1; j >= 0; --j) {// 由尾及首
                int index = word.charAt(j) - 'a';
                if (cur.children[index] == null) {// 无则创建
                    cur.children[index] = new TrieNode65();
                    cur.hasNext = true;// 不是最末尾的节点
                }
                cur = cur.children[index];
            }
            nodes.put(cur, word.length());
        }

        int ans = 0;
        for (TrieNode65 node : nodes.keySet())
            if (node.hasNext == false)// 是最末尾的节点，这样就不会重复统计可以被包含的子串
                ans += nodes.get(node) + 1;

        return ans;
    }

    // 方法二：（自己写的）哈希 + 排序 + 贪心-时间复杂度：O(nm)，空间复杂度：O(n)
    // n为词数，m为每个词的长度，s为总字符数

    // 排序，优先统计长字符串，是否可以包含words中的字符串，可以包含则从哈希中剔除
    public int minimumLengthEncoding2(String[] words) {
        Set<String> set = new HashSet<>();
        for (String word : words)
            set.add(word);

        // 排序
        Arrays.sort(words, (w1, w2) -> w2.length() - w1.length());

        int res = 0;
        for (String word : words) {
            // 哈希中不包含该字符串，说明遍历更长字符串时，已经包含该串并剔除了
            if (!set.contains(word))
                continue;

            int n = word.length();
            res += n + 1;
            set.remove(word);
            // 以该字符串为基准，寻找所有可能的子串
            for (int i = 1; i < n; i++) {
                String subword = word.substring(i, n);
                set.remove(subword);
            }
        }
        return res;

    }

    // 剑指Offer II 066.单词之和
    // 剑指Offer II 067.最大的异或

}

// 哈希表
class HashTableOfferII {

    // 剑指Offer II 010.和为k的子数组（主站560. hot100有）
    // 给定一个整数数组和一个整数 k ，请找到该数组中和为 k 的连续子数组的个数。
    // 提示:
    // 1 <= nums.length <= 2 * 104
    // -1000 <= nums[i] <= 1000
    // -107 <= k <= 107

    // 方法一：枚举-时间复杂度：O(n^2)，空间复杂度：O(1)

    // 方法二：前缀和 + 哈希表优化-时间复杂度：O(n)，空间复杂度：O(n)
    // 前缀和作差的思想
    public int subarraySum(int[] nums, int k) {
        int count = 0;
        int pre = 0;// pre为 [0..i] 里所有数的和
        HashMap<Integer, Integer> mp = new HashMap<>();// 以前缀和pre为键，对应的值为出现次数
        mp.put(0, 1);// 该键值对的作用是，pre直接等于k时，能使count++
        for (int i = 0; i < nums.length; i++) {
            pre += nums[i];
            if (mp.containsKey(pre - k)) // 当前前缀和pre与之前前缀和作差，能得到所有以当前元素结尾的子数组
                count += mp.get(pre - k);

            mp.put(pre, mp.getOrDefault(pre, 0) + 1);
        }
        return count;
    }

    // 剑指offer l1 011. 0和1个数相同的子数组（主站525.）
    // 给定一个二进制数组 nums , 找到含有相同数量的 0 和 1 的最长连续子数组，并返回该子数组的长度。
    // 由于「0 和 1 的数量相同」等价于「1 的数量减去 0 的数量等于 0」，我们可以将数组中的 0 视作 −1，
    // 则原问题转换成「求最长的连续子数组，其元素和为 0」。

    // 方法一：前缀和 + 哈希表-时间复杂度：O(n)，空间复杂度：O(n)
    public int findMaxLength(int[] nums) {
        int maxLength = 0;
        Map<Integer, Integer> map = new HashMap<Integer, Integer>();// 以前缀和counter为键，对应的值为前缀和末尾下标
        int counter = 0;// [0..i] 里所有数的和
        map.put(counter, -1);// 和为0，末尾下标为-1，该键值对的作用是，当counter直接等于0时，得到当前答案
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            int num = nums[i];
            if (num == 1)
                counter++;
            else
                counter--;
            if (map.containsKey(counter)) {// counter - x = 0，则 x = counter，哈希表存在counter时，不更新counter下标（要使答案length尽可能长）
                int prevIndex = map.get(counter);
                maxLength = Math.max(maxLength, i - prevIndex);// 当前答案为 末尾下标之差
            } else// 没有counter才加入
                map.put(counter, i);
        }
        return maxLength;
    }

    // 剑指Offer II 014.字符串中的变位词（主站567.）
    // 给定两个字符串 s1 和 s2，写一个函数来判断 s2 是否包含 s1 的某个变位词。
    // 换句话说，第一个字符串的排列之一是第二个字符串的 子串 。
    // 提示：
    // s1 和 s2 仅包含小写字母

    // 方法一：滑动窗口-时间复杂度：O(mn)，空间复杂度：O(1)
    // s1 和 s2 仅包含小写字母，则可不用哈希表，字符串映射成长度为26的数组
    public boolean checkInclusion(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        if (n > m)
            return false;

        int[] cnt1 = new int[26];
        int[] cnt2 = new int[26];
        for (int i = 0; i < n; ++i) {// 字符串映射成数组
            ++cnt1[s1.charAt(i) - 'a'];
            ++cnt2[s2.charAt(i) - 'a'];
        }

        if (Arrays.equals(cnt1, cnt2))
            return true;

        for (int i = n; i < m; ++i) {// 滑动窗口大小为n
            ++cnt2[s2.charAt(i) - 'a'];
            --cnt2[s2.charAt(i - n) - 'a'];
            if (Arrays.equals(cnt1, cnt2))
                return true;
        }
        return false;
    }

    // 方法二：滑动窗口 优化 -时间复杂度：O(n+m)，空间复杂度：O(1)
    // 意到每次窗口滑动时，只统计了一进一出两个字符, 却比较了整个cnt1和cnt2 数组。
    // 从这个角度出发，我们可以用一个变量diff 来记录cnt1与cnt2 的值不同的个数，这样判断cnt1 和cnt2是 否相等就转换成了判断diff是否为0
    // 此外，为简化上述逻辑，我们可以只用一个数组cnt,其中cnt[x]= cnt2[x]- cnt1[x],
    // 将cnt1[x] 与cnt2[x] 的比较替换成cnt[x]与0的比较。

    public boolean checkInclusion2(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        if (n > m)
            return false;

        int[] cnt = new int[26];// cnt[x]= cnt2[x]- cnt1[x]
        for (int i = 0; i < n; ++i) {// 字符串映射成数组
            --cnt[s1.charAt(i) - 'a'];
            ++cnt[s2.charAt(i) - 'a'];
        }
        int diff = 0;// cnt的值不同的个数
        for (int c : cnt)
            if (c != 0)
                ++diff;

        if (diff == 0)
            return true;

        for (int i = n; i < m; ++i) {// 滑动窗口大小为n
            int x = s2.charAt(i) - 'a';// 入窗字符
            int y = s2.charAt(i - n) - 'a';// 出窗字符
            if (x == y)// diff结果不变，跳过
                continue;

            // 处理入窗字符
            if (cnt[x] == 0)
                ++diff;
            ++cnt[x];
            if (cnt[x] == 0)
                --diff;

            // 处理出窗字符
            if (cnt[y] == 0)
                ++diff;
            --cnt[y];
            if (cnt[y] == 0)
                --diff;

            if (diff == 0)
                return true;

        }
        return false;
    }

    // 方法三：双指针-时间复杂度：O(n+m)，空间复杂度：O(1)
    // 回顾方法一的思路，我们在保证区间长度为 n 的情况下，去考察是否存在一个区间使得 cnt 的值全为 0。
    // 反过来，还可以在保证 cnt 的值不为正的情况下，去考察是否存在一个区间，其长度恰好为 n。
    public boolean checkInclusion3(String s1, String s2) {
        int n = s1.length(), m = s2.length();
        if (n > m)
            return false;

        int[] cnt = new int[26];// cnt[x]= cnt2[x]- cnt1[x]
        for (int i = 0; i < n; ++i)
            --cnt[s1.charAt(i) - 'a'];

        // (left, right]
        int left = 0;
        for (int right = 0; right < m; ++right) {// 移动右指针
            // 入窗
            int x = s2.charAt(right) - 'a';
            ++cnt[x];

            // cnt[x]为正，则当前窗口内字符x的数量大于s1中x的数量
            while (cnt[x] > 0) {// 移动左指针，直到cnt[x]=0（保证每次右指针移动入窗的字符x，数量一定不能多余s1中的x，即cnt[x]不为正）
                --cnt[s2.charAt(left) - 'a'];
                ++left;
            }
            if (right - left + 1 == n)
                return true;
        }
        return false;
    }

    // 剑指Offer II 015.字符串中的所有变位词（主站438. hot100有）
    // 给定两个字符串 s 和 p，找到 s 中所有 p 的 变位词 的子串，返回这些子串的起始索引。不考虑答案输出的顺序。
    // 变位词 指字母相同，但排列不同的字符串。
    // 提示:
    // 1 <= s.length, p.length <= 3 * 104
    // s 和 p 仅包含小写字母

    // 方法一：滑动窗口 + 数组-时间复杂度：O(mn)，空间复杂度：O(1)
    // 因为字符串中的字符全是小写字母，可以用长度为26的数组记录字母出现的次数
    // 设n = len(s), m = len(p)。记录p字符串的字母频次p_cnt，和s字符串前m个字母频次s_cnt
    // 若p_cnt和s_cnt相等，则找到第一个异位词索引 0
    // 继续遍历s字符串索引为[m, n)的字母，在s_cnt中每次增加一个新字母，去除一个旧字母
    // 判断p_cnt和s_cnt是否相等，相等则在返回值res中新增异位词索引 i - m + 1
    public List<Integer> findAnagrams(String s, String p) {
        int n = s.length(), m = p.length();
        List<Integer> res = new ArrayList<>();
        if (n < m)
            return res;

        // 初始化两个数组
        int[] pCnt = new int[26];
        int[] sCnt = new int[26];

        for (int i = 0; i < m; i++) {// 字符串映射成数组
            pCnt[p.charAt(i) - 'a']++;
            sCnt[s.charAt(i) - 'a']++;
        }

        if (Arrays.equals(sCnt, pCnt))
            res.add(0);

        for (int i = m; i < n; i++) {// 滑动窗口大小为m
            sCnt[s.charAt(i - m) - 'a']--;
            sCnt[s.charAt(i) - 'a']++;
            if (Arrays.equals(sCnt, pCnt))
                res.add(i - m + 1);// 添加起始索引
        }
        return res;
    }

    // 方法二：滑动窗口 + 数组 优化 -时间复杂度：O(m+n)，空间复杂度：O(1)
    public List<Integer> findAnagrams2(String s, String p) {
        int n = s.length(), m = p.length();
        List<Integer> res = new ArrayList<>();
        if (n < m)
            return res;

        int[] cnt = new int[26];// cnt[x]= cnt2[x]- cnt1[x]
        for (int i = 0; i < m; ++i) {// 字符串映射成数组
            --cnt[p.charAt(i) - 'a'];
            ++cnt[s.charAt(i) - 'a'];
        }
        int diff = 0;// cnt的值不同的个数
        for (int c : cnt)
            if (c != 0)
                ++diff;

        if (diff == 0)
            res.add(0);

        for (int i = m; i < n; ++i) {// 滑动窗口大小为m
            int x = s.charAt(i) - 'a';// 入窗字符
            int y = s.charAt(i - m) - 'a';// 出窗字符
            if (x == y) {// diff结果不变，跳过
                if (diff == 0)
                    res.add(i - m + 1);
                continue;
            }

            // 处理入窗字符
            if (cnt[x] == 0)
                ++diff;
            ++cnt[x];
            if (cnt[x] == 0)
                --diff;

            // 处理出窗字符
            if (cnt[y] == 0)
                ++diff;
            --cnt[y];
            if (cnt[y] == 0)
                --diff;

            if (diff == 0)
                res.add(i - m + 1);

        }
        return res;
    }

    // 方法三：滑动窗口 + 双指针-时间复杂度：O(m+n)，空间复杂度：O(1)
    // 判断正确答案的条件：窗口大小刚好等于p的长度（而不是比较计数数组）
    public List<Integer> findAnagrams3(String s, String p) {
        int n = s.length(), m = p.length();
        List<Integer> res = new ArrayList<>();
        if (n < m)
            return res;

        int[] pCnt = new int[26];
        int[] sCnt = new int[26];

        // 初始化p数组
        for (int i = 0; i < m; i++)
            pCnt[p.charAt(i) - 'a']++;

        int left = 0;
        for (int right = 0; right < n; right++) {
            int curRight = s.charAt(right) - 'a';
            sCnt[curRight]++;
            while (sCnt[curRight] > pCnt[curRight]) {// （保证每次右指针移动入窗的字符x，数量一定不能多余s1中的x，即cnt[x]不为正）
                int curLeft = s.charAt(left) - 'a';
                sCnt[curLeft]--;
                left++;
            }
            if (right - left + 1 == m)// 判断正确答案的条件：窗口大小刚好等于p的长度（而不是比较计数数组）
                res.add(left); // 添加起始索引
        }
        return res;
    }

    // 剑指Offer II 016.不含重复字符的最长子字符串（主站3. hot100有）
    // 给定一个字符串 s ，请你找出其中不含有重复字符的 最长连续子字符串 的长度。

    // 方法一：滑动窗口-时间复杂度：O(N)，空间复杂度：O(∣Σ∣)，∣Σ∣ 表示字符集的大小。
    public int lengthOfLongestSubstring(String s) {
        HashMap<Character, Integer> map = new HashMap<>();
        int max = 0, start = 0;
        for (int end = 0; end < s.length(); end++) {
            char ch = s.charAt(end);
            if (map.containsKey(ch)) {
                start = Math.max(map.get(ch) + 1, start);
            }
            max = Math.max(max, end - start + 1);
            map.put(ch, end);
        }
        return max;
    }

    // 剑指Offer II 017.含有所有字符的最短字符串（主站76. hot100有）
    // 给定两个字符串 s 和 t 。返回 s 中包含 t 的所有字符的最短子字符串。如果 s 中不存在符合条件的子字符串，则返回空字符串 "" 。
    // 如果 s 中存在多个符合条件的子字符串，返回任意一个。
    // 注意： 对于 t 中重复字符，我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。

    // 方法一：滑动窗口-时间复杂度：O(C⋅∣s∣+∣t∣)，字符集大小为 C，空间复杂度：O(C)
    // 我们在 s 上滑动窗口，通过移动 r 指针不断扩张窗口。当窗口包含 t 全部所需的字符后，
    // 如果能收缩，我们就收缩窗口直到得到最小窗口。
    Map<Character, Integer> ori = new HashMap<Character, Integer>();// 字符串t的哈希表
    Map<Character, Integer> cnt = new HashMap<Character, Integer>();// （当前窗口内）字符计数的哈希表

    public String minWindow(String s, String t) {
        // 填充ori
        for (int i = 0; i < t.length(); i++) {
            char c = t.charAt(i);
            ori.put(c, ori.getOrDefault(c, 0) + 1);
        }

        int l = 0, r = -1;
        int len = Integer.MAX_VALUE, ansL = -1, ansR = -1;// 窗口大小，窗口左右边界
        int sLen = s.length();
        while (r < sLen) {
            ++r; // 一次移动一步
            if (r < sLen && ori.containsKey(s.charAt(r)))// 当前字符为目标t中的字符，加入计数哈希表
                cnt.put(s.charAt(r), cnt.getOrDefault(s.charAt(r), 0) + 1);

            while (check()) {// 当前窗口涵盖 t 所有字符的子串
                // 窗口大小变小，更新窗口边界
                if (r - l + 1 < len) {
                    len = r - l + 1;
                    ansL = l;
                    ansR = l + len;
                }
                // 准备移动窗口左边界，更新计数哈希表
                if (ori.containsKey(s.charAt(l)))
                    cnt.put(s.charAt(l), cnt.getOrDefault(s.charAt(l), 0) - 1);
                ++l;// 一次移动一步
            }
        }
        return ansL == -1 ? "" : s.substring(ansL, ansR);
    }

    // 检查当前窗口是否涵盖 t 所有字符的子串
    public boolean check() {
        Iterator<Map.Entry<Character, Integer>> iter = ori.entrySet().iterator();
        while (iter.hasNext()) {
            Map.Entry<Character, Integer> entry = iter.next();
            Character key = entry.getKey();
            Integer val = entry.getValue();
            if (cnt.getOrDefault(key, 0) < val)
                return false;

        }
        return true;
    }

    // 方法二：优化方法一，预处理 s，扔掉那些 t 中没有出现的字符，然后再做滑动窗口呢？
    // 或者在移动窗口时，一次跳多步

    // 剑指offer II 022.链表中环的入口节点（主站142. hot100有）
    // 给定一个链表，返回链表开始入环的第一个节点。 从链表的头节点开始沿着 next 指针进入环的第一个节点为环的入口节点。
    // 如果链表无环，则返回 null。
    // 为了表示给定链表中的环，我们使用整数 pos 来表示链表尾连接到链表中的位置（索引从 0 开始）。 如果 pos 是 -1，则在该链表中没有环。
    // 注意，pos 仅仅是用于标识环的情况，并不会作为参数传递到函数中。
    // 说明：不允许修改给定的链表。

    // 方法一：哈希表-时间复杂度：O(N)，空间复杂度：O(N)
    public ListNode detectCycle(ListNode head) {
        Set<ListNode> seen = new HashSet<ListNode>();
        while (head != null) {
            if (!seen.add(head)) // 添加失败
                return head;

            head = head.next;
        }
        return null;
    }

    // 方法二：快慢指针-时间复杂度：O(N)，空间复杂度：O(1)
    // 如果链表中存在环，则 fast 指针最终将再次与 slow 指针在环中相遇。
    // 设链表中环外部分的长度为 a。slow 指针进入环后，又走了 b 的距离与fast 相遇。
    // 此时，fast 指针已经走完了环的 n 圈，因此它走过的总距离为 a+n(b+c)+b=a+(n+1)b+nc。
    // 根据题意，任意时刻，fast 指针走过的距离都为 slow 指针的 2 倍。
    // 因此，我们有
    // a+(n+1)b+nc=2(a+b)⟹a=c+(n−1)(b+c)
    // 有了 a=c+(n-1)(b+c)a=c+(n−1)(b+c) 的等量关系，我们会发现：
    // 从相遇点到入环点的距离加上 n-1 圈的环长，恰好等于从链表头部到入环点的距离。

    // 因此，当发现 slow 与 fast 相遇时，我们再额外使用一个指针 ptr。起始，它指向链表头部；
    // 随后，它和 slow 每次向后移动一个位置。最终，它们会在入环点相遇。
    public ListNode detectCycle2(ListNode head) {
        if (head == null)
            return null;

        ListNode slow = head, fast = head;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
            if (fast == slow) {// 含环，此时开始寻找入环节点
                ListNode ptr = head;
                while (ptr != slow) {
                    ptr = ptr.next;
                    slow = slow.next;
                }
                return ptr;
            }
        }
        return null;
    }

    // 剑指Offer II 023.两个链表的第一个重合节点（主站160. hot100有）
    // 给定两个单链表的头节点 headA 和 headB ，请找出并返回两个单链表相交的起始节点。如果两个链表没有交点，返回 null 。
    // 题目数据 保证 整个链式结构中不存在环。
    // 注意，函数返回结果后，链表必须 保持其原始结构 。

    // 方法一：哈希集合-时间复杂度：O(m+n)，空间复杂度：O(m)
    // 判断两个链表是否相交，可以使用哈希集合存储链表节点。
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        Set<ListNode> visited = new HashSet<ListNode>();
        ListNode temp = headA;
        // 首先遍历链表 headA，并将链表 headA 中的每个节点加入哈希集合中。
        while (temp != null) {
            visited.add(temp);
            temp = temp.next;
        }
        temp = headB;
        // 然后遍历链表 headB，对于遍历到的每个节点，判断该节点是否在哈希集合中
        while (temp != null) {
            if (visited.contains(temp))
                return temp;

            temp = temp.next;
        }
        return null;
    }

    // 方法二：双指针-时间复杂度：O(m+n)，空间复杂度：O(1)
    // 链表 headA 和 headB 的长度分别是 m 和 n。
    // 假设链表 headA 的不相交部分有 a 个节点，链表 headB 的不相交部分有 b 个节点，两个链表相交的部分有 cc 个节点，
    // 则有 a+c=m，b+c=n。
    // 如果 a=b，则两个指针会同时到达两个链表相交的节点，此时返回相交的节点；
    // 如果 a!=b，则指针 pA 会遍历完链表 headA，指针 pB 会遍历完链表 headB，两个指针不会同时到达链表的尾节点，
    // 然后指针 pA 移到链表 headB 的头节点，指针 pB 移到链表 headA 的头节点，然后两个指针继续移动，
    // 在指针 pA 移动了 a+c+b 次、指针 pB 移动了b+c+a 次之后，两个指针会同时到达两个链表相交的节点，
    // 该节点也是两个指针第一次同时指向的节点，此时返回相交的节点。
    public ListNode getIntersectionNode2(ListNode headA, ListNode headB) {
        // 一定不相交
        if (headA == null || headB == null)
            return null;

        ListNode pA = headA, pB = headB;
        while (pA != pB) {
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
        }
        return pA;
    }

    // 剑指Offer ll 030.插入、删除和..
    // 剑指Offer II 031.最近最少使用..

    // 剑指offer II 032.有效的变位词（主站242. 相似）
    // 给定两个字符串 s 和 t ，编写一个函数来判断它们是不是一组变位词（字母异位词）。
    // 注意：若 s 和 t 中每个字符出现的次数都相同「且字符顺序不完全相同」，则称 s 和 t 互为变位词（字母异位词）。
    // 提示:
    // 1 <= s.length, t.length <= 5 * 104
    // s and t 仅包含小写字母
    // 进阶: 如果输入字符串包含 unicode 字符怎么办？你能否调整你的解法来应对这种情况？

    // 方法一：排序-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    // 排序后比较是否相等

    // 方法二：哈希表（仅包含小写字母）-时间复杂度：O(n)，空间复杂度：O(1)
    // 映射到长度为26的数组即可
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length())
            return false;

        int[] table = new int[26];
        for (int i = 0; i < s.length(); i++)
            table[s.charAt(i) - 'a']++;

        for (int i = 0; i < t.length(); i++) {
            table[t.charAt(i) - 'a']--;
            if (table[t.charAt(i) - 'a'] < 0)
                return false;
        }

        if (s.equals(t))
            return false;

        return true;
    }

    // 方法二：哈希表（输入字符串包含 unicode 字符）-时间复杂度：O(n)，空间复杂度：O(1)
    // 使用HashMap
    public boolean isAnagram2(String s, String t) {
        if (s.length() != t.length())
            return false;

        Map<Character, Integer> table = new HashMap<Character, Integer>();
        for (int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            table.put(ch, table.getOrDefault(ch, 0) + 1);
        }
        for (int i = 0; i < t.length(); i++) {
            char ch = t.charAt(i);
            table.put(ch, table.getOrDefault(ch, 0) - 1);
            if (table.get(ch) < 0)
                return false;
        }
        if (s.equals(t))
            return false;
        return true;
    }

    // 剑指Offer II 033.变位词组（主站49. hot100有）
    // 给定一个字符串数组 strs ，将 变位词 组合在一起。 可以按任意顺序返回结果列表。
    // 注意：若两个字符串中每个字符出现的次数都相同，则称它们互为变位词。

    // 本题关键在于用什么作键（键尽可能短）

    // 方法一：排序（排序之后的字符串作为哈希表的键）-时间复杂度：O(nklogk)，空间复杂度：O(nk)
    public List<List<String>> groupAnagrams(String[] strs) {
        Map<String, List<String>> map = new HashMap<String, List<String>>();
        for (String str : strs) {
            char[] array = str.toCharArray();
            Arrays.sort(array);
            String key = new String(array);
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList<>(map.values());
    }

    // 方法二：计数（出现次数大于 0 的字母和出现次数按顺序拼接成字符串，作为哈希表的键）
    // 时间复杂度：O(n(k+∣Σ∣))，空间复杂度：O(n(k+∣Σ∣))
    public List<List<String>> groupAnagrams2(String[] strs) {
        Map<String, List<String>> map = new HashMap<String, List<String>>();
        for (String str : strs) {
            int[] counts = new int[26];
            int length = str.length();
            for (int i = 0; i < length; i++)
                counts[str.charAt(i) - 'a']++;

            // 将每个出现次数大于 0 的字母和出现次数按顺序拼接成字符串，作为哈希表的键
            // 不能只用出现次数拼接， 0 10， 0 1 0 无法区分
            StringBuffer sb = new StringBuffer();
            for (int i = 0; i < 26; i++)
                if (counts[i] != 0) {// 精髓所在：只拼接非0次数，尽可能减少字符键的长度！
                    sb.append((char) ('a' + i));
                    sb.append(counts[i]);
                }

            String key = sb.toString();
            List<String> list = map.getOrDefault(key, new ArrayList<String>());
            list.add(str);
            map.put(key, list);
        }
        return new ArrayList<List<String>>(map.values());
    }

    // 方法三：自己的lowb想法 并查集（字母异位词可找到共键，直接哈希表.values()转List得到答案）

    // 剑指Offer II 034.外星语言是否排序（主站953.）
    // 某种外星语也使用英文小写字母，但可能顺序 order 不同。字母表的顺序（order）是一些小写字母的排列。
    // 给定一组用外星语书写的单词 words，以及其字母表的顺序 order，
    // 只有当给定的单词在这种外星语中按字典序排列时，返回 true；否则，返回 false。

    // 方法一： 检查相邻单词-时间复杂度：O(mn)，空间复杂度：O(1)
    // 其实难点主要在字符串比较
    public boolean isAlienSorted(String[] words, String order) {
        int[] index = new int[26];
        for (int i = 0; i < order.length(); ++i)// 各字符 “权重” 映射
            index[order.charAt(i) - 'a'] = i;

        // 标签，可以continue break 指定循环层
        search: for (int i = 0; i < words.length - 1; ++i) {
            String word1 = words[i];
            String word2 = words[i + 1];

            // 逐个字符比较 word1 word2
            for (int k = 0; k < Math.min(word1.length(), word2.length()); ++k)
                if (word1.charAt(k) != word2.charAt(k)) {// 比较第一个不同的字符即可比较大小
                    if (index[word1.charAt(k) - 'a'] > index[word2.charAt(k) - 'a'])
                        return false;
                    continue search;// 已完成字符串比较，无需对后续字符进行比较
                }

            // 在有限长度内，仍未比较出结果 e.g. ("app", "apple").
            if (word1.length() > word2.length())
                return false;
        }

        return true;
    }

    // 方法一：（自己写的）检查相邻单词-时间复杂度：O(mn)，空间复杂度：O(1)
    int[] letterWeight = new int[26];

    public boolean isAlienSorted11(String[] words, String order) {
        for (int i = 0; i < order.length(); i++)
            letterWeight[order.charAt(i) - 'a'] = i;

        for (int i = 0; i < words.length - 1; i++)
            if (!isOrdered(words[i], words[i + 1]))
                return false;

        return true;
    }

    private boolean isOrdered(String s1, String s2) {
        int n1 = s1.length();
        int n2 = s2.length();
        for (int i = 0; i < Math.min(n1, n2); i++) {
            int weight1 = letterWeight[s1.charAt(i) - 'a'];
            int weight2 = letterWeight[s2.charAt(i) - 'a'];
            if (weight1 > weight2)
                return false;
            else if (weight1 < weight2)
                return true;
        }
        // 能到这里说明在前n个字符比较都相等
        if (n1 > n2)
            return false;
        else// s1 长度小于等于 s2
            return true;
    }

    // 剑指Offer II 056.二叉搜索树中..

    // 剑指Offer II 060.出现频率最高的 k 个数字（主站347.）
    // 给定一个整数数组 nums 和一个整数 k ，请返回其中出现频率前 k 高的元素。可以按 任意顺序 返回答案。

    // 方法一：堆 PriorityQueue -时间复杂度：O(Nlogk)，空间复杂度：O(N)
    // 首先遍历整个数组，并使用哈希表记录每个数字出现的次数，并形成一个「出现次数数组」。
    // 找出原数组的前 k 个高频元素，就相当于找出「出现次数数组」的前 k 大的值。
    // 最简单的做法是给「出现次数数组」排序。但由于可能有 O(N) 个不同的出现次数（其中 N 为原数组长度），
    // 故总的算法复杂度会达到 O(NlogN)，不满足题目的要求。

    // 选取k个最大值的另一种思路：（不是数组映射到根数，而是直接使用优先队列）
    // 建立一个小顶堆，然后遍历「出现次数数组」：
    // 如果堆的元素个数小于 k，就可以直接插入堆中。
    // 如果堆的元素个数等于 k，则检查堆顶与当前出现次数的大小。
    // 如果堆顶更大，说明至少有 k 个数字的出现次数比当前值大，故舍弃当前值；否则，就弹出堆顶，并将当前值插入堆中。
    // 遍历完成后，堆中的元素就代表了「出现次数数组」中前 k 大的值。
    public int[] topKFrequent(int[] nums, int k) {
        // 哈希表记录每个数字出现的次数，并形成一个「出现次数数组」。
        Map<Integer, Integer> occurrences = new HashMap<Integer, Integer>();
        for (int num : nums)
            occurrences.put(num, occurrences.getOrDefault(num, 0) + 1);

        // int[] 的第一个元素代表数组的值，第二个元素代表了该值出现的次数
        PriorityQueue<int[]> queue = new PriorityQueue<int[]>((m, n) -> m[1] - n[1]);

        for (Map.Entry<Integer, Integer> entry : occurrences.entrySet()) {
            int num = entry.getKey(), count = entry.getValue();
            if (queue.size() == k) {
                if (queue.peek()[1] < count) {// 弃掉堆顶和当前值中更小的
                    queue.poll();
                    queue.offer(new int[] { num, count });
                }

            } else// 优先队列中不足k个则直接入队
                queue.offer(new int[] { num, count });

        }

        int[] ret = new int[k];
        for (int i = 0; i < k; ++i)
            ret[i] = queue.poll()[0];

        return ret;
    }

    // 方法一：（自己写的）堆 PriorityQueue -时间复杂度：O(Nlogk)，空间复杂度：O(N)
    public int[] topKFrequent11(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums)
            map.put(num, map.getOrDefault(num, 0) + 1);

        PriorityQueue<Map.Entry<Integer, Integer>> pq = new PriorityQueue<>((m, n) -> m.getValue() - n.getValue());
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            pq.offer(entry);
            while (pq.size() > k)
                pq.poll();
        }

        int[] res = new int[k];
        for (int i = 0; i < k; i++)
            res[i] = pq.poll().getKey();

        return res;
    }

    // 方法二：（自己写的）基于快速排序-时间复杂度：O(N^2)（最坏），空间复杂度：O(N)
    // 使用基于快速排序的方法，求出「出现次数数组」的前 k 大的值。
    int n, k;
    int[][] array;

    public int[] topKFrequent2(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        for (int num : nums)
            map.put(num, map.getOrDefault(num, 0) + 1);

        this.n = map.size();
        this.k = k;
        this.array = new int[n][2];
        Iterator<Map.Entry<Integer, Integer>> iter = map.entrySet().iterator();
        for (int i = 0; i < n; i++) {
            Map.Entry<Integer, Integer> entry = iter.next();
            array[i] = new int[] { entry.getKey(), entry.getValue() };
        }
        quickSort(0, n - 1);
        int[] res = new int[k];
        for (int i = 0; i < k; i++)
            res[i] = array[i][0];

        return res;
    }

    // 找到第k个元素的快排
    private void quickSort(int left, int right) {
        if (left >= right)
            return;

        int pivotIndex = left + (-left + right) / 2;
        int pivotValue = array[pivotIndex][1];
        swap(pivotIndex, right);
        int index = left;
        for (int i = left; i < right; i++)
            if (array[i][1] > pivotValue) {
                swap(index, i);
                index++;
            }

        swap(index, right);
        if (index == k - 1)
            return;
        else if (index < k - 1)
            quickSort(index + 1, right);
        else
            quickSort(left, index - 1);

    }

    private void swap(int i, int j) {
        int[] temp = array[i];
        array[i] = array[j];
        array[j] = temp;
    }

    // 剑指Offer II 062.实现前缀树
    // 剑指Offer II 063.替换单词
    // 剑指Offer II 064.神奇的字典
    // 剑指Offer II 065.最短的单词编码
    // 剑指offer II 066.单词之和
    // 剑指Offer II 067.最大的异或

    // 剑指Offer II 075.数组相对排序（主站1122.）
    // 给定两个数组，arr1 和 arr2，
    // arr2 中的元素各不相同
    // arr2 中的每个元素都出现在 arr1 中
    // 对 arr1 中的元素进行排序，使 arr1 中项的相对顺序和 arr2 中的相对顺序相同。（大小比较参照arr2）
    // 未在 arr2 中出现过的元素需要按照升序放在 arr1 的末尾。
    // 提示：
    // 1 <= arr1.length, arr2.length <= 1000
    // 0 <= arr1[i], arr2[i] <= 1000
    // arr2 中的元素 arr2[i] 各不相同（arr1中的元素可能相同）
    // arr2 中的每个元素 arr2[i] 都出现在 arr1 中

    // 方法一：计数排序-时间复杂度：O(n)，空间复杂度：O(n)
    // 注意到本题中元素的范围为 [0, 1000]，这个范围不是很大，我们也可以考虑不基于比较的排序，例如「计数排序」。
    // 通过置0的方式，只是用一个计数数组即可完成排序
    public int[] relativeSortArray(int[] arr1, int[] arr2) {
        int upper = 0;// 最大值
        for (int x : arr1)
            upper = Math.max(upper, x);

        int[] frequency = new int[upper + 1];// 计数数组
        for (int x : arr1)
            ++frequency[x];

        int[] ans = new int[arr1.length];// 答案数组
        int index = 0;

        // 出现在arr2中的数
        for (int x : arr2) {
            for (int i = 0; i < frequency[x]; ++i)// 依次放入答案数组
                ans[index++] = x;

            frequency[x] = 0;// 出现在arr2中的数已经放入ans数组，frequency数组中剩下的非0值即为未出现在arr2中的数
        }

        // 未出现在arr2中的数，按照升序放末尾
        for (int x = 0; x <= upper; ++x)
            for (int i = 0; i < frequency[x]; ++i) // 依次放入答案数组，出现过的数的frequency = 0，自动跳过
                ans[index++] = x;

        return ans;
    }

    // 方法二：（自己写的）自定义比较器，时间复杂度：O(nlogn)，空间复杂度：O(n)
    public int[] relativeSortArray2(int[] arr1, int[] arr2) {
        int[] weight = new int[1001];
        Arrays.fill(weight, Integer.MAX_VALUE);
        // 记录arr2中数的相对顺序
        for (int i = 0; i < arr2.length; i++)
            weight[arr2[i]] = i;

        Integer[] ans = new Integer[arr1.length];
        // Integer[] ans = Arrays.stream(arr1).boxed().toArray(Integer[]::new); // int[]
        // 转 Integer[]
        for (int i = 0; i < arr1.length; i++)
            ans[i] = arr1[i];

        Arrays.sort(ans, (x, y) -> {
            // 未在arr2中出现的数，按照（实际值大小）升序排列
            if (weight[x] == Integer.MAX_VALUE && weight[y] == Integer.MAX_VALUE)
                return x - y;
            else// 按照arr2的相对大小排序
                return weight[x] - weight[y];
        });

        int[] res = new int[arr1.length];
        for (int i = 0; i < arr1.length; i++)
            res[i] = ans[i];

        return res;
    }

    // 剑指Offer II 093.最长斐波那契..
    // 剑指Offer II 108.单词演变
    // 剑指Offer II 109.开密码锁

    // 剑指Offer II 119.最长连续序列（主站128.）
    // 给定一个未排序的整数数组 nums ，找出数字连续的最长序列（不要求序列元素在原数组中连续）的长度。

    // 方法一：哈希表-时间复杂度、空间复杂度：O(n)
    public int longestConsecutive(int[] nums) {
        Set<Integer> num_set = new HashSet<Integer>();// Set去重
        for (int num : nums)
            num_set.add(num);

        int longestStreak = 0;

        for (int num : num_set)
            if (!num_set.contains(num - 1)) {// 不含前驱，开始找它的后继
                int currentNum = num;
                int currentStreak = 1;

                while (num_set.contains(currentNum + 1)) {
                    currentNum += 1;
                    currentStreak += 1;
                }

                longestStreak = Math.max(longestStreak, currentStreak);
            }

        return longestStreak;
    }

}

// 链表
class LinkedListOfferII {
    class Node {
        public int val;
        public Node next;

        public Node() {
        }

        public Node(int _val) {
            val = _val;
        }

        public Node(int _val, Node _next) {
            val = _val;
            next = _next;
        }
    }
    // 剑指Offer II 021.删除链表的倒数第 n 个结点（主站19. hot100有）
    // 给定一个链表，删除链表的倒数第 n 个结点，并且返回链表的头结点。
    // 进阶：能尝试使用一趟扫描实现吗？

    // 方法一：计算链表长度（两趟扫描）-时间复杂度：O(L)，空间复杂度：O(1)
    // 首先从头节点开始对链表进行一次遍历，得到链表的长度 L。
    // 随后我们再从头节点开始对链表进行一次遍历，当遍历到第 L−n+1 个节点时，它就是我们需要删除的节点。
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        int length = getLength(head);
        ListNode cur = dummy;
        for (int i = 1; i < length - n + 1; ++i)
            cur = cur.next;

        cur.next = cur.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    public int getLength(ListNode head) {
        int length = 0;
        while (head != null) {
            ++length;
            head = head.next;
        }
        return length;
    }

    // 方法二：栈（一趟扫描）-时间复杂度：O(L)，空间复杂度：O(L)
    // 我们也可以在遍历链表的同时将所有节点依次入栈。
    // 根据栈「先进后出」的原则，我们弹出栈的第 n 个节点就是需要删除的节点，并且目前栈顶的节点就是待删除节点的前驱节点。
    public ListNode removeNthFromEnd2(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        Deque<ListNode> stack = new LinkedList<ListNode>();
        ListNode cur = dummy;
        while (cur != null) {
            stack.push(cur);
            cur = cur.next;
        }
        for (int i = 0; i < n; ++i)
            stack.pop();

        ListNode prev = stack.peek();
        prev.next = prev.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    // 自己写的dfs-时间复杂度：O(L)，空间复杂度：O(L)
    public ListNode removeNthFromEnd22(ListNode head, int n) {
        ListNode dummyHead = new ListNode(-1, head);
        dfs(dummyHead, n);
        return dummyHead.next;
    }

    int dfs(ListNode head, int n) {
        if (head == null)
            return 0;

        int count = dfs(head.next, n);
        if (count == n)
            head.next = head.next.next;
        return count + 1;
    }

    // 方法三：双指针（一趟扫描，且空间复杂度为O(1)）-时间复杂度：O(L)，空间复杂度：O(1)
    // 由于我们需要找到倒数第 n 个节点，因此我们可以使用两个指针 first 和 second 同时对链表进行遍历，
    // 并且 first 比 second 超前 n 个节点。当 first 遍历到链表的末尾时，second 就恰好处于倒数第 n 个节点。
    // 具体地，初始时 first 和 second 均指向头节点。我们首先使用 first 对链表进行遍历，遍历的次数为 n。
    // 此时， first 和 second 之间间隔了 n−1 个节点，即 first 比 second 超前了 n 个节点。
    // 在这之后，我们同时使用 first 和 second 对链表进行遍历。
    // 当 first 遍历到链表的末尾（即 first 为空指针）时，second 恰好指向倒数第 n 个节点。
    public ListNode removeNthFromEnd3(ListNode head, int n) {
        ListNode dummy = new ListNode(0, head);
        ListNode first = head;
        ListNode second = dummy;

        for (int i = 0; i < n; ++i)
            first = first.next;

        while (first != null) {
            first = first.next;
            second = second.next;
        }
        second.next = second.next.next;
        ListNode ans = dummy.next;
        return ans;
    }

    // 剑指Offer II 022.链表中环的入..
    // 剑指offer II 023.两个链表的第..

    // 剑指Offer II 024.反转链表（主站206. hot100有）
    // 给定单链表的头节点 head ，请反转链表，并返回反转后的链表的头节点。
    // 进阶：链表可以选用迭代或递归方式完成反转。你能否用两种方法解决这道题？

    // 方法一：迭代-时间复杂度：O(n)，空间复杂度：O(1)
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode curr = head;
        while (curr != null) {// 由curr跳出，需要避免next空指针
            ListNode next = curr.next;// 可看做临时节点，存放curr.next
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }

    // 方法二：递归-时间复杂度：O(n)，空间复杂度：O(n)
    public ListNode reverseList2(ListNode head) {
        if (head == null || head.next == null)// 尾节点开始返回
            return head;
        // ListNode nextNode = head.next; 减少额外的空间开销
        ListNode newHead = reverseList2(head.next);
        head.next.next = head;// 下一个节点的next指向当前节点

        // 当前节点指向null 后续会被更改，直到重新回到原头节点，防止新链表末尾（原链表头）成环
        head.next = null;
        return newHead;
    }

    // 剑指offer II 025.链表中的两数..
    // 剑指Offer II 026.重排链表
    // 剑指Offer II 027.回文链表
    // 剑指Offer II 028.展平多级双向... .

    // 剑指Offer II 029.排序的循环链表（主站708.）
    // 给定循环单调非递减列表中的一个点，写一个函数向这个列表中插入一个新元素 insertVal ，使这个列表仍然是循环升序的。
    // 给定的可以是这个列表中任意一个顶点的指针，并不一定是这个列表中最小元素的指针。
    // 如果有多个满足条件的插入位置，可以选择任意一个位置插入新的值，插入后整个列表仍然保持有序。
    // 如果列表为空（给定的节点是 null），需要创建一个循环有序列表并返回这个节点。否则。请返回原先给定的节点。

    // 方法一：（自己写的，1-2次遍历，实现更简单，将问题转化为统一的解法）双指针-时间复杂度：O(n)，空间复杂度：O(1)
    // 找到最小值作为新的头节点，再开始找合适插入位置
    public Node insert(Node head, int insertVal) {
        // 循环链表节点个数为0个的特殊情况
        if (head == null) {
            Node node = new Node(insertVal);
            node.next = node;
            return node;
        }

        // 找到最小值作为新的头节点，由prev.val <= curr.val跳出则找到新的最小值，若由curr != head跳出，则头节点即为最小值
        Node prev = head;
        Node curr = head.next;
        while (prev.val <= curr.val && curr != head) {
            prev = curr;
            curr = curr.next;
        }
        Node newHead = curr;

        // 找到insertVal的插入位置，并插入循环链表
        prev = newHead;
        curr = newHead.next;
        while (curr != newHead) {// 遍历完一边都没找到插入位置，则一定是插入头尾节点之间
            if (prev.val < insertVal && insertVal <= curr.val)
                break;
            prev = curr;
            curr = curr.next;
        }
        Node node = new Node(insertVal, curr);
        prev.next = node;
        return head;
    }

    // 方法二：（自己写的，真正1次遍历，但实现更复杂，将问题分情况讨论）模拟-时间复杂度：O(n)，空间复杂度：O(1)
    Node head;
    int insertVal;

    public Node insert2(Node head, int insertVal) {
        this.head = head;
        this.insertVal = insertVal;

        // 循环链表节点个数为0个的特殊情况
        if (head == null) {
            Node newHead = new Node(insertVal);
            newHead.next = newHead;
            return newHead;
        }

        Node node = head;

        // 1.头节点值等于插入节点值
        if (node.val == insertVal) {
            insert(node);
            return head;
        }

        // 2.头节点值小于插入节点值
        if (node.val < insertVal) {
            findPrevNode(node);
            return head;
        }

        // 3.头节点值大于插入节点值

        // 找到新的最小值，如果有
        while (node.next != head && node.next.val >= node.val)
            node = node.next;

        // 没有新的最小值（已遍历到最末尾），或者新的最小值仍大于插入节点值
        if (node.next == head || node.next.val > insertVal)
            insert(node);
        else
            findPrevNode(node.next);

        return head;

    }

    // 当前节点小于插入节点值，找到插入节点的前驱节点
    private void findPrevNode(Node node) {
        while (node.next != head) {

            if (node.next.val < insertVal && node.next.val >= node.val)
                node = node.next;
            else// 下一个节点大于等于插入值，或者下一个节点小于当前节点值
                break;
        }
        insert(node);
    }

    private void insert(Node node) {
        Node insertNode = new Node(insertVal);
        insertNode.next = node.next;
        node.next = insertNode;
    }

    // 剑指Offer II 031.最近最少使用..

    // 剑指Offer II 077.链表排序（主站148. hot100有）
    // 给定链表的头结点 head ，请将其按 升序 排列并返回 排序后的链表 。
    // 进阶：你可以在 O(n log n) 时间复杂度和常数级空间复杂度下，对链表进行排序吗？

    // 「147. 对链表进行插入排序」要求使用插入排序的方法对链表进行排序，插入排序的时间复杂度是 O(n^2)，其中 n 是链表的长度。
    // 这道题考虑时间复杂度更低的排序算法。题目的进阶问题要求达到 O(nlogn) 的时间复杂度和 O(1) 的空间复杂度，
    // 时间复杂度是 O(nlogn) 的排序算法包括归并排序、堆排序和快速排序（快速排序的最差时间复杂度是 O(n^2)，
    // 其中最适合链表的排序算法是归并排序。（快排、堆排的空间复杂度是O(n)）

    // 方法一：自顶向下归并排序-时间复杂度：O(nlogn)，其中 n 是链表的长度。空间复杂度：O(logn)
    // 对链表自顶向下归并排序的过程如下。
    // 找到链表的中点，以中点为分界，将链表拆分成两个子链表。
    // 寻找链表的中点可以使用快慢指针的做法，快指针每次移动 2 步，慢指针每次移动 1 步，
    // 当快指针到达链表末尾时，慢指针指向的链表节点即为链表的中点。
    // 对两个子链表分别排序。
    // 将两个排序后的子链表合并，得到完整的排序后的链表。可以使用「21. 合并两个有序链表」的做法，将两个有序的子链表进行合并。
    // 上述过程可以通过递归实现。
    // 递归的终止条件是链表的节点个数小于或等于 1，即当链表为空或者链表只包含 1 个节点时，不需要对链表进行拆分和排序。
    public ListNode sortList(ListNode head) {
        return sortList(head, null);
    }

    public ListNode sortList(ListNode head, ListNode tail) {
        if (head == null)
            return head;

        // 由于特殊的拆分方式，拆分到仅剩两个元素时，直接去掉末尾元素
        // 不可能拆分成一个元素
        if (head.next == tail) {
            head.next = null;// “断开”链表，方便后续归并时，根据到null判断一边链表到尾了
            return head;
        }

        // 使用快慢指针找到中点进行拆分
        ListNode slow = head, fast = head;
        while (fast != tail) {
            slow = slow.next;
            fast = fast.next;
            if (fast != tail)
                fast = fast.next;
        }
        // 这里的mid可以理解成向上取整，而不是一般归并中的向下取整
        ListNode mid = slow;
        // 拆分（方式稍有变化，为了防止死循环）
        ListNode list1 = sortList(head, mid);
        ListNode list2 = sortList(mid, tail);
        // 合并
        ListNode sorted = merge(list1, list2);
        return sorted;
    }

    // 合并两个有序链表（迭代）
    public ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummyHead = new ListNode(0);
        ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
        while (temp1 != null && temp2 != null) {
            if (temp1.val <= temp2.val) {
                temp.next = temp1;
                temp1 = temp1.next;
            } else {
                temp.next = temp2;
                temp2 = temp2.next;
            }
            temp = temp.next;
        }

        // 处理末尾
        if (temp1 != null)
            temp.next = temp1;
        else if (temp2 != null)
            temp.next = temp2;

        return dummyHead.next;
    }

    // 方法一：（自己写的）自顶向下归并排序-时间复杂度：O(nlogn)，其中 n 是链表的长度。空间复杂度：O(logn)
    // 划分时，不指明结束节点，而是直接置为null表示结束
    public ListNode sortList11(ListNode head) {
        return partition(head);
    }

    private ListNode partition(ListNode head) {
        if (head == null || head.next == null)
            return head;
        // 防止死循环拆分，e.g. 1-2-null, slow = 2 fast = null, first = 1-2-null second = null
        // 即看能否把2节点的链表拆分
        ListNode fast = head.next;// 快指针先走一步
        ListNode slow = head;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
        }
        ListNode first = head;
        ListNode second = slow.next;
        slow.next = null;// 置为null表示结束
        ListNode l1 = partition(first);
        ListNode l2 = partition(second);
        return merge(l1, l2);
    }

    // 方法二：自底向上归并排序-时间复杂度：O(nlogn)，空间复杂度：O(1)
    // 使用自底向上的方法实现归并排序，则可以达到 O(1) 的空间复杂度。
    // 首先求得链表的长度 length，然后将链表拆分成子链表进行合并。
    public ListNode sortList2(ListNode head) {
        if (head == null)
            return head;

        // 1. 首先从头向后遍历,统计链表长度
        int length = 0; // 用于统计链表长度
        ListNode node = head;
        while (node != null) {
            length++;
            node = node.next;
        }

        // 2. 初始化 引入dummynode
        ListNode dummyHead = new ListNode(0);
        dummyHead.next = head;

        // 3. 每次将链表拆分成若干个长度为subLen的子链表 , 并按照每两个子链表一组进行合并
        // subLen每次左移一位（即sublen = sublen*2） PS:位运算对CPU来说效率更高
        for (int subLen = 1; subLen < length; subLen <<= 1) {
            ListNode prev = dummyHead;
            ListNode curr = dummyHead.next; // curr用于记录拆分链表的位置

            while (curr != null) { // 如果链表没有被拆完
                // 3.1 拆分subLen长度的链表1
                ListNode head_1 = curr; // 第一个链表的头 即 curr初始的位置
                for (int i = 1; i < subLen && curr != null && curr.next != null; i++) // 拆分出长度为subLen的链表1
                    curr = curr.next;

                // 3.2 拆分subLen长度的链表2
                ListNode head_2 = curr.next; // 第二个链表的头 即 链表1尾部的下一个位置
                curr.next = null; // 断开第一个链表和第二个链表的链接
                curr = head_2; // 第二个链表头 重新赋值给curr
                for (int i = 1; i < subLen && curr != null && curr.next != null; i++) // 再拆分出长度为subLen的链表2
                    curr = curr.next;

                // 3.3 再次断开 第二个链表最后的next的链接
                ListNode next = null;
                if (curr != null) {
                    next = curr.next; // next用于记录 拆分完两个链表的结束位置
                    curr.next = null; // 断开链接
                }

                // 3.4 合并两个subLen长度的有序链表
                ListNode merged = merge(head_1, head_2);
                prev.next = merged; // prev.next 指向排好序链表的头
                while (prev.next != null) // while循环 将prev移动到 subLen*2 的位置后去
                    prev = prev.next;

                curr = next; // next用于记录 拆分完两个链表的结束位置
            }
        }
        // 返回新排好序的链表
        return dummyHead.next;
    }

    // 合并两个有序链表（见方法一）
    // public ListNode merge(ListNode l1, ListNode l2)

    // 剑指Ooffer II 078.合并排序链表（主站23. hot100有）
    // 给定一个链表数组，每个链表都已经按升序排列。
    // 请将所有链表合并到一个升序链表中，返回合并后的链表。
    // 提示：
    // k == lists.length
    // 0 <= k <= 10^4
    // 0 <= lists[i].length <= 500
    // -10^4 <= lists[i][j] <= 10^4
    // lists[i] 按 升序 排列
    // lists[i].length 的总和不超过 10^4

    // 方法一：顺序合并-时间复杂度为 O(kn×k)，空间复杂度：O(1)
    // （合并k个升序链表分解为多次合并两个升序链表）
    // 用一个变量 ans 来维护以及合并的链表，第 i 次循环把第 i 个链表和 ans 合并，答案保存到 ans 中。

    // 方法二：分治合并-时间复杂度为 O(kn×logk)，空间复杂度：O(logk)
    // 优化方法一，用分治的方法进行合并。（思路：归并排序）
    public ListNode mergeKLists(ListNode[] lists) {
        return merge(lists, 0, lists.length - 1);
    }

    // l r为链表数组的索引
    public ListNode merge(ListNode[] lists, int l, int r) {
        if (l == r)
            return lists[l];

        if (l > r)// 为了应对k=0的情况，此时l=0, r=-1
            return null;

        int mid = (l + r) >> 1;
        return mergeTwoLists(merge(lists, l, mid), merge(lists, mid + 1, r));
    }

    public ListNode mergeTwoLists(ListNode a, ListNode b) {
        if (a == null || b == null)
            return a != null ? a : b;

        ListNode head = new ListNode(0);
        ListNode tail = head, aPtr = a, bPtr = b;
        while (aPtr != null && bPtr != null) {
            if (aPtr.val < bPtr.val) {
                tail.next = aPtr;
                aPtr = aPtr.next;
            } else {
                tail.next = bPtr;
                bPtr = bPtr.next;
            }
            tail = tail.next;
        }
        tail.next = (aPtr != null ? aPtr : bPtr);
        return head.next;
    }

    // 方法三：使用优先队列合并-时间复杂度为 O(kn×logk)，空间复杂度为 O(k)
    // 类似bfs，注意要使优先队列中元素尽可能的少，如果一开始就把所有元素放入优先队列再输出，就是O(kn×logn)了
    public ListNode mergeKLists3(ListNode[] lists) {
        if (lists == null || lists.length == 0)
            return null;
        // 优先队列中元素为Comparable接口实现类，或者传入Comparator
        PriorityQueue<ListNode> queue = new PriorityQueue<>(lists.length, (node1, node2) -> node1.val - node2.val);
        // 各链表（最小）首元素入队
        for (ListNode node : lists)
            if (node != null)
                queue.offer(node);

        ListNode head = new ListNode(0);// dummy head
        ListNode tail = head;
        while (!queue.isEmpty()) {
            ListNode node = queue.poll();// 最小元素出队
            tail.next = node;
            tail = tail.next;
            if (node.next != null) // 出队元素后继节点入队
                queue.offer(node.next);

        }
        return head.next;
    }

}

// 双指针（滑动窗口）
class DoublePointerOfferII {
    // 剑指Offer II 006.排序数组中两...
    // 剑指Offer II 007.数组中和为0.. .

    // 剑指Offer II 009.乘积小于 K 的子数组（主站713.）
    // 给定一个正整数数组 nums和整数 k ，请找出该数组内乘积小于 k 的连续的子数组的个数。
    // 提示:
    // 1 <= nums.length <= 3 * 104
    // 1 <= nums[i] <= 1000
    // 0 <= k <= 106
    // 子数组乘积严格小于 k

    // 方法一：前缀和 + 二分查找（对数运算比较麻烦）-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 我们可以使用二分查找解决这道题目，即对于固定的i,二分查找出最大的j满足nums[i]到nums[j]的乘积小于k。
    // 但由于乘积可能会非常大(在最坏情况下会达到1000^500)，会 导致数值溢出，
    // 因此我们需要对nums数组取对数,将乘法转换为加法,这样就不会出现数值溢出的问题了。
    public int numSubarrayProductLessThanK(int[] nums, int k) {
        if (k == 0)
            return 0;
        // 计算前缀和，转化为对应log
        double logk = Math.log(k);
        double[] prefix = new double[nums.length + 1];
        for (int i = 0; i < nums.length; i++)
            prefix[i + 1] = prefix[i] + Math.log(nums[i]);

        int ans = 0;
        for (int i = 0; i < prefix.length; i++) {
            int left = i + 1, right = prefix.length;
            while (left < right) {
                int mi = left + (right - left) / 2;
                if (prefix[mi] < prefix[i] + logk - 1e-9)
                    left = mi + 1;
                else
                    right = mi;
            }
            ans += left - i - 1;
        }
        return ans;
    }

    // 方法二：滑动窗口-时间复杂度：O(n)，空间复杂度：O(1)
    public int numSubarrayProductLessThanK2(int[] nums, int k) {
        if (k <= 1)
            return 0;
        int prod = 1;// 滑动窗口内数字的乘积
        int ans = 0;
        int left = 0;
        // 以后边界为准，方便统计结果
        for (int right = 0; right < nums.length; right++) {
            prod *= nums[right];
            // 每次移动right后，移动left直到满足prod < k，若left==right时，product仍大于k，则left =
            // right+1，此时product==1小于k，退出循环
            // 计算窗口大小为0，不影响结果统计
            while (prod >= k)
                prod /= nums[left++];
            ans += right - left + 1;// 当前窗口长度即为以right为右边界的子数组个数
        }
        return ans;
    }

    // 剑指Offer II 014.字符串中的变...

    // 剑指Offer II 018.有效的回文（主站125.）
    // 给定一个字符串 s ，验证 s 是否是 回文串 ，只考虑字母和数字字符，可以忽略字母的大小写。
    // 本题中，将空字符串定义为有效的 回文串 。
    // 判断回文串直接用双指针从两边开始比较即可
    // 最大回文串则需要以每个字符为中心开始逐个比较（优化：马拉车算法）

    // 方法一：筛选 + 判断-时间复杂度：O(|s|)，空间复杂度：O(|s|)
    public boolean isPalindrome(String s) {
        // 处理字符串s，去除非字符和数字
        StringBuffer sgood = new StringBuffer();
        int length = s.length();
        for (int i = 0; i < length; i++) {
            char ch = s.charAt(i);
            if (Character.isLetterOrDigit(ch))
                sgood.append(Character.toLowerCase(ch));
        }
        int n = sgood.length();
        int left = 0, right = n - 1;
        while (left < right) {
            if (sgood.charAt(left) != sgood.charAt(right))
                return false;
            ++left;
            --right;
        }
        return true;
    }

    // 方法二：在原字符串上直接判断-时间复杂度：O(|s|)，空间复杂度：O(1)
    // 最好还是不要嵌套使用while，很多次重复判断 left < right
    public boolean isPalindrome2(String s) {
        int n = s.length();
        int left = 0, right = n - 1;
        while (left < right) {
            // 移动 left 指针，跳过非字符和数字
            while (left < right && !Character.isLetterOrDigit(s.charAt(left)))
                ++left;

            // 移动 right 指针，跳过非字符和数字
            while (left < right && !Character.isLetterOrDigit(s.charAt(right)))
                --right;

            if (left < right) {
                if (Character.toLowerCase(s.charAt(left)) != Character.toLowerCase(s.charAt(right)))
                    return false;
                ++left;
                --right;
            }
        }
        return true;
    }

    // 方法二：（自己写的）双指针-时间复杂度：O(|s|)，空间复杂度：O(1)
    public boolean isPalindrome22(String s) {
        int left = 0;
        int right = s.length() - 1;

        while (left < right) {
            char lc = s.charAt(left);
            if (Character.isLetterOrDigit(lc)) {
                if (Character.isLetter(lc))
                    lc = Character.toLowerCase(lc);
            } else {
                left++;
                continue;
            }

            char rc = s.charAt(right);
            if (Character.isLetterOrDigit(rc)) {
                if (Character.isLetter(rc))
                    rc = Character.toLowerCase(rc);
            } else {
                right--;
                continue;
            }

            if (lc != rc)
                return false;
            left++;
            right--;
        }
        return true;

    }

    // 剑指Offer II 019.最多删除一个字符得到回文（主站680.）
    // 给定一个非空字符串 s，请判断如果 最多 从字符串中删除一个字符能否得到一个回文字符串。

    // 方法一：双指针 + 贪心-时间复杂度：O(n)，空间复杂度：O(1)
    public boolean validPalindrome(String s) {
        return validPalindrome(s, 0, s.length() - 1, true);
    }

    public boolean validPalindrome(String s, int low, int high, boolean canDelete) {
        while (low < high) {
            char c1 = s.charAt(low), c2 = s.charAt(high);
            if (c1 == c2) {
                ++low;
                --high;
            } else {
                if (canDelete)// 分别尝试：删除low指向字符，删除high指向字符
                    return validPalindrome(s, low + 1, high, false) || validPalindrome(s, low, high - 1, false);
                else
                    return false;
            }
        }
        return true;
    }

    // 剑指Offer II 021.删除链表的倒...
    // 剑指Offer II 022.链表中环的入...
    // 剑指Offer II 023.两个链表的第...
    // 剑指Offer II 026.重排链表
    // 剑指Offer II 027.回文链表
    // 剑指Offer II 056.二叉搜索树中...
    // 剑指Offer II 077.链表排序

}

// 二分查找
class BinarySearchOfferII {
    // 剑指Offer II 006.排序数组中两个数字之和（主站167.相似）
    // 给定一个已按照 升序排列  的整数数组 numbers ，请你从数组中找出两个数满足相加之和等于目标数 target 。
    // 函数应该以长度为 2 的整数数组的形式返回这两个数的下标值。numbers 的下标 从 0 开始计数 ，
    // 所以答案数组应当满足 0 <= answer[0] < answer[1] < numbers.length 。
    // 假设数组中存在且只存在一对符合条件的数字，同时一个数字不能使用两次。

    // 你所设计的解决方案必须只使用常量级的额外空间。不要求则可使用哈希表
    // 方法一：二分查找-时间复杂度：O(nlogn)，空间复杂度：O(1)
    public int[] twoSum(int[] numbers, int target) {
        for (int i = 0; i < numbers.length; ++i) {
            int low = i + 1, high = numbers.length - 1;
            while (low <= high) {
                int mid = (high - low) / 2 + low;
                if (numbers[mid] == target - numbers[i])
                    return new int[] { i, mid };
                else if (numbers[mid] > target - numbers[i])
                    high = mid - 1;
                else
                    low = mid + 1;

            }
        }
        return new int[] { -1, -1 };
    }

    // 方法二：双指针-时间复杂度：O(n)，空间复杂度：O(1)
    // 充分利用 升序排列 的条件
    public int[] twoSum2(int[] numbers, int target) {
        int low = 0, high = numbers.length - 1;
        while (low < high) {
            int sum = numbers[low] + numbers[high];
            if (sum == target)
                return new int[] { low, high };
            else if (sum < target)
                ++low;
            else
                --high;

        }
        return new int[] { -1, -1 };
    }

    // 方法三：哈希表-时间复杂度：O(n)，空间复杂度：O(n)
    public int[] twoSum3(int[] nums, int target) {
        Map<Integer, Integer> hashtable = new HashMap<Integer, Integer>();
        // 建表与查表同时进行，因为答案唯一a + b = target，先到a或b都行
        for (int i = 0; i < nums.length; ++i) {
            if (hashtable.containsKey(target - nums[i]))
                return new int[] { hashtable.get(target - nums[i]), i };

            hashtable.put(nums[i], i);
        }
        return new int[] { -1, -1 };
    }

    // 剑指Offer II 008.和大于等于 target 的最短子数组
    // 给定一个含有 n 个正整数的数组和一个正整数 target 。
    // 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ，
    // 并返回其长度。如果不存在符合条件的子数组，返回 0 。
    // 进阶：
    // 如果你已经实现 O(n) 时间复杂度的解法, 请尝试设计一个 O(n log(n)) 时间复杂度的解法。

    // 方法一：暴力法-时间复杂度：O(n2)，空间复杂度：O(1)

    // 方法二：前缀和 + 二分查找-时间复杂度：O(nlogn)，空间复杂度：O(n)
    // 用TreeSet存储前缀和，用ceiling和floor查找也是logn
    public int minSubArrayLen2(int s, int[] nums) {
        int n = nums.length;
        if (n == 0)
            return 0;

        int ans = Integer.MAX_VALUE;

        // 为了方便计算，令 size = n + 1
        // sums[0] = 0 意味着前 0 个元素的前缀和为 0
        // sums[1] = A[0] 前 1 个元素的前缀和为 A[0]
        // 以此类推
        int[] sums = new int[n + 1];
        for (int i = 1; i <= n; i++)
            sums[i] = sums[i - 1] + nums[i - 1];

        for (int i = 1; i <= n; i++) {
            int target = s + sums[i - 1];
            int bound = Arrays.binarySearch(sums, target);
            if (bound < 0)// 转化为应当插入的位置
                bound = -bound - 1;

            if (bound <= n)
                ans = Math.min(ans, bound - (i - 1));
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

    // 方法二：（自己写的，一次遍历）前缀和 + 二分查找-时间复杂度：O(nlogn)，空间复杂度：O(n)
    public int minSubArrayLen22(int target, int[] nums) {
        int n = nums.length;
        int[] sumArr = new int[n];

        int sum = 0;
        int res = Integer.MAX_VALUE;

        for (int i = 0; i < n; i++) {
            sum += nums[i];
            if (i == 0 && sum >= target)// 第一个元素即大于target，直接返回1即可
                return 1;

            sumArr[i] = sum;// 二分查找限制了范围，不用担心影响提前放入sum数组，而最终查找结果
            int index = 0;
            if (sum - target >= 0)// 当前sum大于target才有查找的必要
                index = Arrays.binarySearch(sumArr, 0, i, sum - target);
            else
                continue;

            if (index < 0) {// 应当插入的位置
                index = -index - 1;
                res = Math.min(res, i - index + 1);
            } else// 直接找到
                res = Math.min(res, i - index);

        }
        return res == Integer.MAX_VALUE ? 0 : res;
    }

    // 方法三：滑动窗口-时间复杂度：O(n)，空间复杂度：O(1)
    public int minSubArrayLen3(int s, int[] nums) {
        int n = nums.length;
        if (n == 0)
            return 0;

        int ans = Integer.MAX_VALUE;
        int start = 0, end = 0;
        int sum = 0;
        while (end < n) {
            sum += nums[end];
            while (sum >= s) {
                ans = Math.min(ans, end - start + 1);
                sum -= nums[start];
                start++;
            }
            end++;
        }
        return ans == Integer.MAX_VALUE ? 0 : ans;
    }

    // 方法三：（自己写的）滑动窗口-时间复杂度：O(n)，空间复杂度：O(1)
    public int minSubArrayLen33(int target, int[] nums) {
        int n = nums.length;
        int left = 0;
        int right = 0;
        int currSum = 0;
        int res = Integer.MAX_VALUE;

        for (right = 0; right < n; right++) {
            currSum += nums[right];
            if (currSum < target)
                continue;

            while (left < right) {
                if (currSum - nums[left] >= target)
                    currSum -= nums[left++];
                else
                    break;
            }
            res = Math.min(res, right - left + 1);
        }
        return res == Integer.MAX_VALUE ? 0 : res;
    }
    // 剑指Offer II 059.数据流的第K ..
    // 剑指Offer II 060.出现频率最高..

    // 剑指Offer II 061.和最小的 k 个数对（主站373.）
    // 给定两个以升序排列的整数数组 nums1 和 nums2 , 以及一个整数 k 。
    // 定义一对值 (u,v)，其中第一个元素来自 nums1，第二个元素来自 nums2 。
    // 请找到和最小的 k 个数对 (u1,v1),  (u2,v2)  ...  (uk,vk) 。

    // 方法一：优先队列-时间复杂度：O(klogk)，空间复杂度：O(k)
    // 优先队列的bfs，循环遍历k次即可找到最小的 k 个数对
    public List<List<Integer>> kSmallestPairs(int[] nums1, int[] nums2, int k) {
        // 小根堆，存放的是索引，数对：( nums1[o[0]], nums2[o[1]] )
        PriorityQueue<int[]> pq = new PriorityQueue<>(k,
                (o1, o2) -> nums1[o1[0]] + nums2[o1[1]] - nums1[o2[0]] - nums2[o2[1]]);
        List<List<Integer>> ans = new ArrayList<>();
        int m = nums1.length;
        int n = nums2.length;
        // 这里 Math.min(m, k)，尽一切可能减少堆的大小
        for (int i = 0; i < Math.min(m, k); i++)// nums1所有节点和nums2[0]节点组成的数对
            pq.offer(new int[] { i, 0 });

        while (k-- > 0 && !pq.isEmpty()) {
            int[] idxPair = pq.poll();
            List<Integer> list = new ArrayList<>();
            list.add(nums1[idxPair[0]]);
            list.add(nums2[idxPair[1]]);
            ans.add(list);
            if (idxPair[1] + 1 < n) // 相邻节点（比当前数对大一点点）入队，只入队一个数对，（右数+1），避免相同数对入队
                pq.offer(new int[] { idxPair[0], idxPair[1] + 1 });
        }
        return ans;
    }

    // 方法二：二分查找-空间复杂度：O(1)
    // 感觉还是挺复杂的
    // 1. 二分查找第 k 小的数对和的大小
    // 2. 找到小于目标值 pairSum 的数对
    // 3. 找到等于目标值 pairSum 的数对
    public List<List<Integer>> kSmallestPairs2(int[] nums1, int[] nums2, int k) {
        int m = nums1.length;
        int n = nums2.length;

        /* 二分查找第 k 小的数对和的大小 */
        // 寻找的方法：（主站378.有序矩阵中第 K 小的元素 相似，二分查找 + 对角线斜着走）
        // (0,0) (0,1) (0,2)
        // (1,0) (1,1) (1,2)
        // (2,0) (2,1) (2,2)
        int left = nums1[0] + nums2[0];// (0, 0)
        int right = nums1[m - 1] + nums2[n - 1];// (m-1, n-1)
        int pairSum = right;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            long cnt = 0;
            // 起始位置(0, n-1)（右上角）
            int i = 0;
            int j = n - 1;
            // （主站378.有序矩阵中第 K 小的元素 相似，二分查找 + 对角线斜着走）
            // 计算小于等于mid（即数对(i, j)）的数对个数，跟k比较
            while (i < m && j >= 0) {// 退出循环，1. 行数到头了，2. 列数到头了（提前结束循环）
                if (nums1[i] + nums2[j] > mid)//不计数，继续向左移动
                    j--;
                else {// 计数
                      // (i, j)数对 <= mid，将当前所在行的不大于 mid 的数对的数量（即 j+1）累加到答案中，并向下移动
                    cnt += j + 1;
                    i++;
                }
            }
            if (cnt < k)
                left = mid + 1;
            else {
                pairSum = mid;
                right = mid - 1;
            }
        }

        List<List<Integer>> ans = new ArrayList<>();
        int pos = n - 1;
        /* 找到小于目标值 pairSum 的数对 */
        // 结束后，一定有k>=0，k=0，则无等于 pairSum 的数对
        for (int i = 0; i < m; i++) {// 先确定左数
            while (pos >= 0 && nums1[i] + nums2[pos] >= pairSum)// 确定右数的范围
                pos--;

            for (int j = 0; j <= pos; j++, k--) {
                List<Integer> list = new ArrayList<>();
                list.add(nums1[i]);
                list.add(nums2[j]);
                ans.add(list);
            }
        }

        /* 找到等于目标值 pairSum 的数对 */
        // k = 0 后不会进入循环
        pos = n - 1;
        for (int i = 0; i < m && k > 0; i++) {
            while (pos >= 0 && nums1[i] + nums2[pos] > pairSum)
                pos--;
            for (int j = i; k > 0 && j >= 0 && nums1[j] + nums2[pos] == pairSum; j--, k--) {
                List<Integer> list = new ArrayList<>();
                list.add(nums1[j]);
                list.add(nums2[pos]);
                ans.add(list);
            }
        }
        return ans;
    }

    // 剑指Offer II 068.查找插入位置（主站35.）
    // 找到第一个大于等于target的数
    // 给定一个排序的整数数组 nums 和一个整数目标值 target ，请在数组中找到 target ，并返回其下标。
    // 如果目标值不存在于数组中，返回它将会被按顺序插入的位置。
    // 请必须使用时间复杂度为 O(log n) 的算法。
    // 提示:
    // 1 <= nums.length <= 104
    // -104 <= nums[i] <= 104
    // nums 为「无重复元素的升序排列」数组
    // -104 <= target <= 104

    // 方法一：二分查找-时间复杂度：O(logn)，空间复杂度：O(1)
    public int searchInsert(int[] nums, int target) {
        int n = nums.length;
        int left = 0, right = n - 1;
        while (left <= right) {
            int mid = ((right - left) >> 1) + left;
            if (target <= nums[mid])
                right = mid - 1;
            else
                left = mid + 1;
        }
        return left;
    }

    // 剑指Offer II 069.山峰数组的顶部（主站852.）
    // 符合下列属性的数组 arr 称为 山峰数组（山脉数组） ：
    // arr.length >= 3
    // 存在 i（0 < i < arr.length - 1）使得：
    // arr[0] < arr[1] < ... arr[i-1] < arr[i]
    // arr[i] > arr[i+1] > ... > arr[arr.length - 1]
    // 给定由整数组成的山峰数组 arr ，
    // 返回任何满足 arr[0] < arr[1] < ... arr[i - 1]
    // < arr[i] > arr[i + 1] > ... > arr[arr.length - 1] 的下标 i ， 即山峰顶部。
    // 提示：
    // 3 <= arr.length <= 104
    // 0 <= arr[i] <= 106
    // 题目数据保证 arr 是一个山脉数组
    // 进阶：很容易想到时间复杂度 O(n) 的解决方案，你可以设计一个 O(log(n)) 的解决方案吗？

    // 方法一：二分查找-时间复杂度：O(logn)，空间复杂度：O(1)
    public int peakIndexInMountainArray(int[] arr) {
        int n = arr.length;
        // 根据题意，山峰顶部不会出现在数组左右边界
        int left = 1, right = n - 2;// 后续不用判越界
        while (left <= right) {
            // 位运算结果一定要放在括号里，再和其他值运算
            int mid = ((right - left) >> 1) + left;
            if (arr[mid] > arr[mid + 1])// 当前比后继高
                right = mid - 1;
            else
                left = mid + 1;
        }
        return left;
    }

    // 方法一：（自己写的）二分查找-时间复杂度：O(logn)，空间复杂度：O(1)
    // 主要考虑越界问题，即山峰顶部可能会出现在左右边界
    public int peakIndexInMountainArray2(int[] arr) {
        int left = 0;
        int right = arr.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            // 防越界处理
            int prevNum = -1;
            int nextNum = -1;
            if (mid > 0)
                prevNum = arr[mid - 1];
            if (mid < arr.length - 1)
                nextNum = arr[mid + 1];

            if (arr[mid] > prevNum && arr[mid] > nextNum)
                return mid;
            // 因为做了防越界处理，如果mid=0，此时prevNum=-1，如果nextNum<arr[mid]，那么在之前就依旧返回最终答案了
            // 所以此时必在山峰顶部左边
            else if (prevNum < arr[mid])
                left = mid + 1;
            else// 不是山峰顶部，也不在山峰顶部左边，那只能在山峰顶部右边啦
                right = mid - 1;
        }
        return left;
    }

    // 剑指Offer II 070.排序数组中只出现一次的数字（主站540.）
    // 给定一个只包含整数的有序数组 nums ，每个元素都会出现两次，唯有一个数只会出现一次，请找出这个唯一的数字。
    // 提示:
    // 1 <= nums.length <= 105
    // 0 <= nums[i] <= 105
    // 进阶: 采用的方案可以在 O(log n) 时间复杂度和 O(1) 空间复杂度中运行吗？

    // 方法一：全数组的二分查找-时间复杂度：O(logn)，空间复杂度：O(1)
    // 假设只出现一次的元素位于下标 x，由于其余每个元素都出现两次，
    // 因此下标 x 的左边和右边都有偶数个元素，数组的长度是奇数。
    // 非常巧妙的情况分类！使得各个情况的操作统一
    // 每次取左右边界的平均值 mid 作为待判断的下标，根据 mid 的奇偶性决定和左边或右边的相邻元素比较：
    // 如果 mid 是偶数，则比较 nums[mid] 和 nums[mid+1] 是否相等；
    // 如果 mid 是奇数，则比较 nums[mid−1] 和 nums[mid] 是否相等。
    public int singleNonDuplicate(int[] nums) {
        int low = 0, high = nums.length - 1;
        while (low < high) {
            // mid可能为0，但一定不会为 nums.length - 1 （偶数，比右，越界）
            int mid = (high - low) / 2 + low;
            // 异或1的操作非常巧妙
            // 如果 mid 是偶数，则比较 nums[mid] 和 nums[mid+1] 是否相等；
            // 如果 mid 是奇数，则比较 nums[mid−1] 和 nums[mid] 是否相等。
            if (nums[mid] == nums[mid ^ 1])
                low = mid + 1;
            else
                high = mid;
        }
        return nums[low];
    }

    // 方法一：（自己写的）全数组的二分查找-时间复杂度：O(logn)，空间复杂度：O(1)
    // 情况分类便于理解，但稍复杂
    public int singleNonDuplicate11(int[] nums) {
        int left = 0;
        int right = nums.length - 1;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            // 防越界处理
            int prevNum = -1;
            int nextNum = Integer.MAX_VALUE;
            if (mid > 0)
                prevNum = nums[mid - 1];
            if (mid < nums.length - 1)
                nextNum = nums[mid + 1];

            int firstIndex = -1;// 出现两次元素的前者的索引值
            if (nums[mid] != prevNum && nums[mid] != nextNum)
                return nums[mid];
            else if (prevNum == nums[mid])
                firstIndex = mid - 1;
            else// (nums[mid]==nextNum)
                firstIndex = mid;

            if ((firstIndex) % 2 == 1)// firstIndex 为奇，则表示mid处在最终答案右边
                right = mid - 1;
            else// firstIndex 为偶，则表示mid处在最终答案左边
                left = mid + 1;
        }

        return nums[left];
    }

    // 方法二：偶数下标的二分查找-时间复杂度：O(logn)，空间复杂度：O(1)

    public int singleNonDuplicate2(int[] nums) {
        int low = 0, high = nums.length - 1;
        while (low < high) {// 防越界的设计
            int mid = (high - low) / 2 + low;
            // 巧妙的位运算
            mid -= mid & 1;// 变为偶数 e.g. 2→2 3→2
            if (nums[mid] == nums[mid + 1])
                low = mid + 2;
            else
                high = mid;// 防越界的设计
        }
        return nums[low];
    }

    // 剑指Offer II 071.按权重生成随机数（主站528.）
    // 给定一个正整数数组 w ，其中 w[i] 代表下标 i 的权重（下标从 0 开始），
    // 请写一个函数 pickIndex ，它可以随机地获取下标 i，选取下标 i 的概率与 w[i] 成正比。
    // 例如，对于 w = [1, 3]，挑选下标 0 的概率为 1 / (1 + 3) = 0.25 （即，25%），
    // 而选取下标 1 的概率为 3 / (1 + 3) = 0.75（即，75%）。
    // 也就是说，选取下标 i 的概率为 w[i] / sum(w) 。
    // 提示：
    // 1 <= w.length <= 10000
    // 1 <= w[i] <= 10^5
    // pickIndex 将被调用不超过 10000 次

    // 方法一：前缀和 + 二分查找（查找第一个大于等于随机数的数，即落在概率分布区间内的数）
    // 时间复杂度：初始化的时间复杂度为 O(n)，每次选择的时间复杂度为 O(logn)，其中 n 是数组 w 的长度。
    // 空间复杂度：O(n)
    class Solution {
        int[] pre;// 前缀和，可看做概率分布
        int total;

        public Solution(int[] w) {
            pre = new int[w.length];
            pre[0] = w[0];
            total += w[0];
            for (int i = 1; i < w.length; ++i) {
                total += w[i];
                pre[i] = pre[i - 1] + w[i];
            }
            // total = Arrays.stream(w).sum();
        }

        public int pickIndex() {
            int x = (int) (Math.random() * total) + 1;// 必须要+1， e.g. 对于13，01的概率应该是1:3， 01,234 → 1,234,5 这样才对
            return binarySearch(x);
        }

        private int binarySearch(int x) {
            int low = 0, high = pre.length - 1;
            while (low <= high) {
                int mid = (high - low) / 2 + low;
                if (pre[mid] < x)
                    low = mid + 1;
                else
                    high = mid - 1;
            }
            return low;
        }
    }

    // 方法一：（自己写的）前缀和 + 二分查找 API
    class Solution11 {
        int sum = 0;
        int n = 0;
        int[] preSum;
        Random rand = new Random();

        public Solution11(int[] w) {
            n = w.length;
            preSum = new int[n];
            int cur = 0;
            for (int i = 0; i < n; i++) {
                cur += w[i];
                preSum[i] = cur;
            }
            sum = cur;
        }

        public int pickIndex() {
            int r = rand.nextInt(sum) + 1;
            int index = Arrays.binarySearch(preSum, 0, n, r);
            if (index >= 0)
                return index;
            else
                return -index - 1; // 应当插入的位置，即最小上界位置
        }
    }

    // 方法二：TreeMap API实现，同理还有 Arrays.binarySearch(Object[] a, Object key)
    class Solution2 {
        int sum;
        TreeMap<Integer, Integer> mp;// <前缀和，索引>

        public Solution2(int[] w) {
            mp = new TreeMap<>();
            sum = 0;
            for (int i = 0; i < w.length; i++) {
                sum += w[i];
                mp.put(sum, i);
            }
        }

        public int pickIndex() {
            Random r = new Random();
            return mp.get(mp.ceilingKey(r.nextInt(sum) + 1));
        }
    }

    // 剑指Offer II 072.求平方根（主站69.）
    // 给定一个非负整数 x ，计算并返回 x 的平方根，即实现 int sqrt(int x) 函数。
    // 正数的平方根有两个，只输出其中的正数平方根。
    // 如果平方根不是整数，输出只保留整数的部分，小数部分将被舍去。
    // 提示：
    // 0 <= x <= 231 - 1

    // 方法一：二分查找-时间复杂度：O(logx)。空间复杂度：O(1)。
    public int mySqrt(int x) {
        int left = 0;
        int right = x;
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if ((long) mid * mid <= x) // 防止数据溢出，也可以直接把右边界设为(231 - 1)^(1/2)
                left = mid + 1;
            else
                right = mid - 1;

        }
        return right;
    }

    // 方法二：袖珍计算器算法-时间复杂度：O(1)。空间复杂度：O(1)。
    // 「袖珍计算器算法」是一种用指数函数 exp 和对数函数 ln 代替平方根函数的方法。
    // 我们通过有限的可以使用的数学函数，得到我们想要计算的结果。
    // 使用自然对数 e 进行换底
    public int mySqrt2(int x) {
        if (x == 0)
            return 0;

        int ans = (int) Math.exp(0.5 * Math.log(x));
        return (long) (ans + 1) * (ans + 1) <= x ? ans + 1 : ans;
        // return (int) Math.pow(x, 0.5);
    }

    // 方法三：牛顿迭代-时间复杂度：O(logx)。空间复杂度：O(1)。
    public int mySqrt3(int x) {
        if (x == 0)
            return 0;

        double C = x, x0 = x;
        while (true) {
            double xi = 0.5 * (x0 + C / x0);
            if (Math.abs(x0 - xi) < 1e-7)
                break;

            x0 = xi;
        }
        return (int) x0;
    }

    // 剑指Offer II 073.狒狒吃香蕉（主站875.）
    // 狒狒喜欢吃香蕉。这里有 N 堆香蕉，第 i 堆中有 piles[i] 根香蕉。警卫已经离开了，将在 H 小时后回来。
    // 狒狒可以决定她吃香蕉的速度 K （单位：根/小时）。每个小时，她将会选择一堆香蕉，从中吃掉 K 根。
    // 如果这堆香蕉少于 K 根，她将吃掉这堆的所有香蕉，然后这一小时内不会再吃更多的香蕉，下一个小时才会开始吃另一堆的香蕉。  
    // 狒狒喜欢慢慢吃，但仍然想在警卫回来前吃掉所有的香蕉。
    // 返回她可以在 H 小时内吃掉所有香蕉的最小速度 K（K 为整数）。
    // 提示：
    // 1 <= piles.length <= 10^4
    // piles.length <= H <= 10^9
    // 1 <= piles[i] <= 10^9

    // 方法一：二分查找-时间复杂度：O(NlogW)，其中 N 是香蕉堆的数量，W 最大的香蕉堆的大小。空间复杂度：O(1)。
    public int minEatingSpeed(int[] piles, int H) {
        int left = 1;
        int right = 1_000_000_000;// 右边界取每堆最大值即可（居然还能这么写）
        while (left <= right) {
            int mid = left + ((right - left) >> 1);
            if (!possible(piles, H, mid))
                left = mid + 1;
            else
                right = mid - 1;
        }

        return left;
    }

    // 是否能以K的进食速度在H小时内吃掉所有香蕉？
    public boolean possible(int[] piles, int H, int K) {
        int time = 0;
        for (int p : piles)
            time += (p - 1) / K + 1;
        return time <= H;
    }

    // 剑指Offer II 076.数组中的第k ..
    // 剑指Offer II 078.合并排序链表

}

// 动态规划
class DynamicProgrammingOfferII {
    // 剑指Offer II 003.前n个数字....

    // 剑指Offer II 020.回文子字符串的个数（主站647.hot100有）
    // 给定一个字符串 s ，请计算这个字符串中有多少个回文子字符串。
    // 具有不同开始位置或结束位置的子串，即使是由相同的字符组成，也会被视作不同的子串。

    // 方法一：中心拓展-时间复杂度：O(n^2)，空间复杂度：O(1)
    public int countSubstrings(String s) {
        int n = s.length(), ans = 0;
        // [0,0], [0,1], [1,1], ..., [n-2,n-1], [n-1, n-1]
        for (int i = 0; i < 2 * n - 1; ++i) {// 所有最小奇偶数中心
            int l = i / 2, r = l + i % 2;// 根据i计算出中心
            while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) {
                --l;
                ++r;
                ++ans;
            }
        }
        return ans;
    }

    // 方法一：（自己写的）中心拓展-时间复杂度：O(n^2)，空间复杂度：O(1)
    String s;

    public int countSubstrings11(String s) {
        this.s = s;
        int res = 0;
        for (int i = 0; i < s.length(); i++) {
            res += count(i, i);
            res += count(i, i + 1);
        }
        return res;
    }

    private int count(int left, int right) {
        int count = 0;
        while (0 <= left && right < s.length()) {
            if (s.charAt(left) == s.charAt(right)) {
                count++;
                left--;
                right++;
            } else
                break;
        }
        return count;
    }

    // 方法二：Manacher 马拉车算法-时间复杂度：O(n)，空间复杂度：O(n)
    // Manacher 算法（5.）是在线性时间内求解最长回文子串的算法。在本题中，改造为求解回文串的个数
    public int countSubstrings2(String s) {
        int n = s.length();
        StringBuffer t = new StringBuffer("$#");
        // 解决回文串奇数长度和偶数长度的问题，处理方式是在所有的相邻字符中间插入 # ，这样可以保证所有找到的回文串都是奇数长度的
        for (int i = 0; i < n; ++i)
            t.append(s.charAt(i)).append('#');

        t.append('!');// 字符串边界外，再随便放两个不一样的字符，防越界。e.g. aba → $#a#b#a#!
        n = t.length();

        int[] f = new int[n];// 用 f(i) 来表示以 t 的第 i 位为回文中心，可以拓展出的最大回文半径（臂长）
        int j = 0, right = 0;// 维护「当前最大的回文的右端点 right」以及这个回文右端点对应的回文中心 j
        int ans = 0;

        for (int i = 2; i < n - 2; ++i) {
            // i 被包含在当前最大回文子串内(right与当前点的距离, i关于j对称的点的f值)，不被包含(0)
            // 这里将 right−i 和 f[对称点] 取小，是先要保证这个回文串在当前最大回文串内。
            f[i] = i <= right ? Math.min(right - i, f[2 * j - i]) : 0; // 初始化 f[i]
            while (t.charAt(i + f[i] + 1) == t.charAt(i - f[i] - 1))// 中心拓展
                ++f[i];
            if (i + f[i] > right) {// 动态维护 iMax 和 rMax
                j = i;
                right = i + f[i];
            }
            ans += (f[i] + 1) / 2;// 统计答案, 臂长为1，贡献一个答案（字符自己），臂长为3，贡献2个...
        }

        return ans;
    }

    // 剑指Offer II 040.矩阵中最大的...
    // 剑指Offer II 051.节点之和最...

    // 剑指Offer II 088.爬楼梯的最少成本（主站746.）
    // 数组的每个下标作为一个阶梯，第 i 个阶梯对应着一个非负数的体力花费值 cost[i]（下标从 0 开始）。
    // 每当爬上一个阶梯都要花费对应的体力值，一旦支付了相应的体力值，就可以选择向上爬一个阶梯或者爬两个阶梯。
    // 请找出达到楼层顶部的最低花费。在开始时，你可以选择从下标为 0 或 1 的元素作为初始阶梯。

    // 方法一：动态规划 + 滚动数组（自己写的）-时间复杂度：O(n)，空间复杂度：O(1)
    public int minCostClimbingStairs(int[] cost) {
        int length = cost.length;
        if (length == 1)
            return cost[0];

        int dp1 = cost[0];
        int dp2 = cost[1];
        for (int i = 2; i < cost.length; i++) {
            int next = Math.min(dp1, dp2) + cost[i];
            dp1 = dp2;
            dp2 = next;
        }
        return Math.min(dp1, dp2);
    }

    // 剑指Offer II 089.房屋偷盗（主站198. hot100有）
    // 一个专业的小偷，计划偷窃沿街的房屋。
    // 每间房内都藏有一定的现金，影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统，
    // 如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警。
    // 给定一个代表每个房屋存放金额的非负整数数组 nums ，请计算 不触动警报装置的情况下 ，一夜之内能够偷窃到的最高金额。

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    // 如果房屋数量大于两间，应该如何计算能够偷窃到的最高总金额呢？对于第 k (k>2) 间房屋，有两个选项：
    // 偷窃第 k 间房屋，那么就不能偷窃第 k-1 间房屋，偷窃总金额为前 k-2 间房屋的最高总金额与第 k 间房屋的金额之和。
    // 不偷窃第 k 间房屋，偷窃总金额为前 k-1 间房屋的最高总金额。
    // 在两个选项中选择偷窃总金额较大的选项，该选项对应的偷窃总金额即为前 kk 间房屋能偷窃到的最高总金额。
    // dp[i] 表示前 i 间房屋能偷窃到的最高总金额，那么就有如下的状态转移方程：
    // dp[i]=max(dp[i−2]+nums[i],dp[i−1])

    public int rob(int[] nums) {
        int length = nums.length;
        if (length == 1)
            return nums[0];

        // 考虑到每间房屋的最高总金额只和该房屋的前两间房屋的最高总金额相关，因此可以使用滚动数组，在每个时刻只需要存储前两间房屋的最高总金额。
        int first = nums[0], second = Math.max(nums[0], nums[1]);
        for (int i = 2; i < length; i++) {
            int temp = second;
            second = Math.max(first + nums[i], second);
            first = temp;
        }
        return second;
    }

    // 剑指Offer II 090.环形房屋偷盗（主站213.）
    // 你是一个专业的小偷，计划偷窃沿街的房屋，每间房内都藏有一定的现金。
    // 这个地方所有的房屋都 围成一圈 ，这意味着第一个房屋和最后一个房屋是紧挨着的。
    // 同时，相邻的房屋装有相互连通的防盗系统，如果两间相邻的房屋在同一晚上被小偷闯入，系统会自动报警 。
    // 给定一个代表每个房屋存放金额的非负整数数组，计算你 在不触动警报装置的情况下 ，今晚能够偷窃到的最高金额。

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    // 分为两种情况讨论即可
    // 如果不偷窃最后一间房屋，则偷窃房屋的下标范围是 [0, n-2]；如果不偷窃第一间房屋，则偷窃房屋的下标范围是 [1, n-1]
    public int rob2(int[] nums) {
        int length = nums.length;
        if (length == 1)
            return nums[0];
        else if (length == 2)
            return Math.max(nums[0], nums[1]);

        return Math.max(robRange(nums, 0, length - 2), robRange(nums, 1, length - 1));
    }

    public int robRange(int[] nums, int start, int end) {
        int first = nums[start], second = Math.max(nums[start], nums[start + 1]);
        for (int i = start + 2; i <= end; i++) {
            int temp = second;
            second = Math.max(first + nums[i], second);
            first = temp;
        }
        return second;
    }

    // 剑指Offer II 091.粉刷房子（主站256.）
    // 假如有一排房子，共 n 个，每个房子可以被粉刷成红色、蓝色或者绿色这三种颜色中的一种，你需要粉刷所有的房子并且使其相邻的两个房子颜色不能相同。
    // 当然，因为市场上不同颜色油漆的价格不同，所以房子粉刷成不同颜色的花费成本也是不同的。
    // 每个房子粉刷成不同颜色的花费是以一个 n x 3 的正整数矩阵 costs 来表示的。
    // 例如，costs[0] 表示第 0 号房子粉刷成红色的成本花费；costs[1][2] 表示第 1 号房子粉刷成绿色的花费，以此类推。
    // 请计算出粉刷完所有房子最少的花费成本。

    // 方法一：动态规划-时间复杂度：O(n)，空间复杂度：O(1)
    // 记录上一间房子三种色粉刷成本的各自最小值即可
    public int minCost(int[][] costs) {
        int[] res = new int[3];
        for (int[] cost : costs) {// 状态转换
            int[] temp = new int[3];
            temp[0] = Math.min(res[1], res[2]) + cost[0];
            temp[1] = Math.min(res[0], res[2]) + cost[1];
            temp[2] = Math.min(res[0], res[1]) + cost[2];
            res = temp;
        }
        return Math.min(res[0], Math.min(res[1], res[2]));
    }

    // 剑指Offer II 092.翻转字符（将字符串翻转到单调递增）（主站926.）
    // 如果一个由 '0' 和 '1' 组成的字符串，是以一些 '0'（可能没有 '0'）后面跟着一些 '1'（也可能没有 '1'）的形式组成的，
    // 那么该字符串是 单调递增 的。
    // 我们给出一个由字符 '0' 和 '1' 组成的字符串 s，我们可以将任何 '0' 翻转为 '1' 或者将 '1' 翻转为 '0'。
    // 返回使 s 单调递增 的最小翻转次数。
    // 提示：
    // 1 <= s.length <= 20000
    // s 中只包含字符 '0' 和 '1'

    // 方法一：动态规划 + 滚动数组-时间复杂度：O(n)，空间复杂度：O(1)
    // 1. 如果当前位为0
    // 如果置为0，前一步必须是0，不用操作
    // 如果置为1，前一步可以是0，也可以是1，翻转操作数+1
    // 2. 如果当前位置为1
    // 如果置为0，前一步必须是0，翻转操作数+1
    // 如果置为1，前一步可以是0，也可以是1，不需要操作
    public int minFlipsMonoIncr(String s) {
        int dp[] = new int[2];
        // 初始化
        dp[0] = s.charAt(0) == '0' ? 0 : 1;// 表示前i个元素，最后一个元素为0的最小翻转次数；
        dp[1] = s.charAt(0) == '1' ? 0 : 1;// 表示前i个元素，最后一个元素为1的最小翻转次数

        // 状态转移
        for (int i = 1; i < s.length(); i++) {
            int temp0 = dp[0] + (s.charAt(i) == '0' ? 0 : 1);
            int temp1 = Math.min(dp[0], dp[1]) + (s.charAt(i) == '1' ? 0 : 1);// 前一步可以是0也可以是1，取更小的就行了
            dp[0] = temp0;
            dp[1] = temp1;
        }
        return Math.min(dp[0], dp[1]);
    }

    // 方法一：（自己写的）动态规划 + 滚动数组-时间复杂度：O(n)，空间复杂度：O(1)
    // 更容易理解
    // 1. 如果当前位为0
    // 如果置为0，前一步必须是0，不用操作
    // 如果置为1，前一步可以是0，也可以是1，翻转操作数+1
    // 2. 如果当前位置为1
    // 如果置为0，前一步必须是0，翻转操作数+1
    // 如果置为1，前一步可以是0，也可以是1，不需要操作
    public int minFlipsMonoIncr11(String s) {
        int dp0 = 0, dp1 = 0;
        for (char c : s.toCharArray()) {
            int newdp0 = 0, newdp1 = 0;
            if (c == '0') {
                newdp0 = dp0;
                newdp1 = Math.min(dp0, dp1) + 1;
            } else {
                newdp0 = dp0 + 1;
                newdp1 = Math.min(dp0, dp1);
            }
            dp0 = newdp0;
            dp1 = newdp1;
        }
        return Math.min(dp0, dp1);
    }

    // 方法二：前缀和-时空复杂度：O(n)
    // 可以依次原始字符串转换成每个答案的代价，其中计算分成两个部分，左边全 0 的部分和右边全 1 的部分。
    // 显然，这个问题可以简化成： 对于每种分割方法，左边有多少个 1 需要去反转，右边有多少个 0 需要去反转。
    // 对这个问题，可以用前缀和来解决。
    public int minFlipsMonoIncr2(String S) {
        int n = S.length();
        int[] P = new int[n + 1];// 定义 P[i+1] = A[0] + A[1] + ... + A[i]，左边有多少个 1 。
        for (int i = 0; i < n; ++i)
            P[i + 1] = P[i] + (S.charAt(i) == '1' ? 1 : 0);

        int ans = Integer.MAX_VALUE;
        // 以j为分界点，左边全置为0 = P[j] ，右边全置为1 = N - j - (P[N] - P[j])（右边总个数 - 右边1个个数）
        for (int j = 0; j <= n; ++j)
            ans = Math.min(ans, P[j] + n - j - (P[n] - P[j]));

        return ans;
    }

    // 方法二：（自己写的）前缀和-时空复杂度：O(n)
    // 代码较长，但更容易理解
    public int minFlipsMonoIncr3(String s) {
        int n = s.length();
        int[] left = new int[n];
        int[] right = new int[n];

        int count = 0;
        for (int i = 0; i < n; i++) {
            left[i] = count;
            if (s.charAt(i) == '1')
                count++;
        }

        count = 0;
        for (int i = n - 1; i >= 0; i--) {
            right[i] = count;
            if (s.charAt(i) == '0')
                count++;
        }

        int res = 20001;
        for (int i = 0; i < n; i++)
            res = Math.min(res, left[i] + right[i]);

        return res;
    }

    // 剑指Offer II 093.最长斐波那契数列（主站873.）
    // 如果序列 X_1, X_2, ..., X_n 满足下列条件，就说它是 斐波那契式 的：
    // 1. n >= 3
    // 2. 对于所有 i + 2 <= n，都有 X_i + X_{i+1} = X_{i+2}
    // 给定一个严格递增的正整数数组形成序列 arr ，找到 arr 中最长的斐波那契式的子序列的长度。如果一个不存在，返回  0 。
    // （回想一下，子序列是从原序列  arr 中派生出来的，它从 arr 中删掉任意数量的元素（也可以不删），而不改变其余元素的顺序。
    // 例如， [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列）

    // 方法一：使用 Set 的暴力法-时间复杂度：O(n^2 logM)，logM ≤ 43，空间复杂度：O(n)
    // 采用记忆化搜索，时间反而增加，可能维护的状态太多了，查找效率降低
    // 每个斐波那契式的子序列都依靠两个相邻项来确定下一个预期项。例如，对于 2, 5，我们所期望的子序列必定以 7, 12, 19, 31 等继续。
    // 我们可以使用 Set 结构来快速确定下一项是否在数组 A 中。由于这些项的值以指数形式增长，最大值≤10^9 的斐波那契式的子序列最多有 43 项。
    public int lenLongestFibSubseq(int[] A) {
        int n = A.length;
        Set<Integer> S = new HashSet<>();
        for (int x : A)// 序列加入集合
            S.add(x);

        int ans = 0;
        for (int i = 0; i < n; ++i)// 以A[i]为第一项
            for (int j = i + 1; j < n; ++j) {// 以A[j]为第二项
                // 以此两项，不断尝试扩展序列
                int x = A[j], y = A[i] + A[j];
                int length = 2;
                while (S.contains(y)) {
                    int tmp = y;
                    y += x;
                    x = tmp;
                    ++length;
                }
                ans = Math.max(ans, length);
            }
        return ans >= 3 ? ans : 0;
    }

    // 方法二：二元动态规划-时间复杂度：O(n^2)，空间复杂度：O(nlogM)
    // 类似于 最长上升子序列 的问题。（主站300.）
    // 提示：
    // 3 <= arr.length <= 1000
    // 1 <= arr[i] < arr[i + 1] <= 10^9
    // j * n + k 该哈希函数可能会导致哈希冲突；且可能会越界，但好像也没有关系，只要能唯一标识该状态即可
    public int lenLongestFibSubseq2(int[] A) {
        int n = A.length;
        Map<Integer, Integer> index = new HashMap<>();// <值, 下标>
        for (int i = 0; i < n; ++i)
            index.put(A[i], i);

        Map<Integer, Integer> longest = new HashMap<>();// <一跳（两个数组成，映射为一个数）, 这一跳结束的最长序列长度>
        int ans = 0;

        // 两跳接力，i, j, k
        for (int k = 2; k < n; ++k)// 以第k个数字为结束跳
            for (int j = 1; j < k; ++j) {// 以第j个数字为中间跳
                int i = index.getOrDefault(A[k] - A[j], -1);// 获取起始跳i
                if (i >= 0 && i < j) {// 有索引，且按顺序
                    // 将 (i, j) 映射为n进制数 i * n + j
                    // 将 (j, k) 映射为n进制数 j * n + k
                    int prev = longest.getOrDefault(i * n + j, 2);// 前一跳(i, j)，默认值+1为3
                    longest.put(j * n + k, prev + 1); // 更新(j, k)，长度+1
                    ans = Math.max(ans, prev + 1);// 更新最终答案
                }
            }

        return ans >= 3 ? ans : 0;
    }

    // 剑指Offer II 094.最少回文分割（主站132.）
    // 给定一个字符串 s，请将 s 分割成一些子串，使每个子串都是回文串。
    // 返回符合要求的 最少分割次数 。

    // 方法一：预处理 + 动态规划-时空复杂度：O(n^2)
    // 思路与算法
    // 设 f[i] 表示字符串的前缀 s[0..i] 的最少分割次数。要想得出 f[i] 的值，我们可以考虑枚举 s[0..i] 分割出的最后一个回文串，
    // 这样我们就可以写出状态转移方程：
    // f[i]=min{f[j]}+1,其中 s[j+1..i] 是一个回文串
    public int minCut(String s) {
        int n = s.length();
        // 时间复杂度优化
        // 马拉车算法优化为O(n)求得所有回文子串
        boolean[][] g = new boolean[n][n];// g[i][j]表示s的子串[i, j]是否为回文串

        // 预处理，将字符串 s 的每个子串是否为回文串预先计算出来
        // [0,0], [0,1], [1,1], ..., [n-2,n-1], [n-1, n-1]
        for (int i = 0; i < 2 * n - 1; ++i) {// 所有最小奇偶数中心
            int l = i / 2, r = l + i % 2;// 根据i计算出中心
            while (l >= 0 && r < n && s.charAt(l) == s.charAt(r)) // 中心扩展
                g[l--][r++] = true;
        }

        int[] f = new int[n];// f[i] 表示字符串的前缀 s[0..i] 的最少分割次数。
        Arrays.fill(f, Integer.MAX_VALUE);
        for (int i = 0; i < n; ++i) {
            if (g[0][i])// 前缀 s[0..i]本身就是回文串，不用分割
                f[i] = 0;
            else
                for (int j = 0; j < i; ++j)
                    if (g[j + 1][i])// f[j]和g[j + 1][i]拼接，即分割次数+1
                        f[i] = Math.min(f[i], f[j] + 1);

        }

        return f[n - 1];
    }

    // 方法一：（自己写的）预处理 + 动态规划-时空复杂度：O(n^2)
    // String s;
    boolean[][] palind;

    public int minCut11(String s) {
        this.s = s;
        int n = s.length();
        this.palind = new boolean[n][n];

        for (int i = 0; i < n; i++) {
            checkPalid(i, i);
            checkPalid(i, i + 1);
        }

        int[] dp = new int[n];
        Arrays.fill(dp, 2022);
        dp[0] = 0;
        for (int j = 1; j < n; j++) {
            if (palind[0][j]) {
                dp[j] = 0;
                continue;
            }

            for (int i = 0; i < j; i++)
                if (palind[i + 1][j])
                    dp[j] = Math.min(dp[j], dp[i] + 1);
        }
        return dp[n - 1];
    }

    private void checkPalid(int left, int right) {
        while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
            palind[left][right] = true;
            left--;
            right++;
        }
    }

    // 剑指Offer II 095.最长公共子序列（主站1143.）
    // 给定两个字符串 text1 和 text2，返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ，返回 0 。
    // 一个字符串的 子序列 是指这样一个新的字符串：
    // 它是由原字符串在不改变字符的相对顺序的情况下删除某些字符（也可以不删除任何字符）后组成的新字符串。
    // 例如，"ace" 是 "abcde" 的子序列，但 "aec" 不是 "abcde" 的子序列。
    // 两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

    // 方法一：动态规划-时空复杂度：O(n^2)
    // 只能使用二维数组存储dp值，无法使用滚动数组节省空间开销
    // 最长公共子序列问题是典型的二维动态规划问题。
    // dp[i][j] 表示 text1[长为i的前缀] 和 text2[长为j的前缀] 的最长公共子序列的长度。
    public int longestCommonSubsequence(String text1, String text2) {
        int m = text1.length(), n = text2.length();
        int[][] dp = new int[m + 1][n + 1];// dp[0][] dp[][0]默认为0，方便 处理边界
        for (int i = 1; i <= m; i++) {
            char c1 = text1.charAt(i - 1);
            for (int j = 1; j <= n; j++) {
                char c2 = text2.charAt(j - 1);
                if (c1 == c2)
                    dp[i][j] = dp[i - 1][j - 1] + 1;// 继承自左上角状态且长度+1
                else
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);// 继承左或上状态

            }
        }
        return dp[m][n];
    }

    // 剑指Offer II 096.字符串交织（主站97.）
    // 给定三个字符串 s1、s2、s3，请判断 s3 能不能由 s1 和 s2 交织（交错） 组成。
    // 两个字符串 s 和 t 交织 的定义与过程如下，其中每个字符串都会被分割成若干 非空 子字符串：
    // s = s1 + s2 + ... + sn
    // t = t1 + t2 + ... + tm
    // |n - m| <= 1
    // 交织 是 s1 + t1 + s2 + t2 + s3 + t3 + ... 或者 t1 + s1 + t2 + s2 + t3 + s3 + ...
    // 提示：a + b 意味着字符串 a 和 b 连接。

    // 方法一：动态规划-时间复杂度：O(nm)，空间复杂度：O(m)，即s2的长度。
    // s1 的前 i 个元素和 s2 的前 j 个元素是否能交错组成s3的前 i+j 个元素，
    // 取决于 s1 的前 i-1 个元素和 s2 的前 j 个元素是否能交错组成 s3 的前 i+j-1 个元素
    // 定义 f(i, j) 表示 s1 的前 i 个元素和 s2 的前 j 个元素是否能交错组成 s3 的前 i + j 个元素。
    public boolean isInterleave(String s1, String s2, String s3) {
        int n = s1.length(), m = s2.length(), t = s3.length();
        if (n + m != t)
            return false;

        // 定义 f(i, j) 表示 s1 的前 i 个元素和 s2 的前 j 个元素是否能交错组成 s3 的前 i + j 个元素。
        boolean[][] f = new boolean[n + 1][m + 1];

        f[0][0] = true;// 其余为false
        for (int i = 0; i <= n; ++i) {// s1 的前 i 个元素
            for (int j = 0; j <= m; ++j) {// s2 的前 j 个元素
                int p = i + j - 1;// s3 的第 i + j 个元素的下标
                if (i > 0)// 防越界
                    // s3 的第 i+j 个元素由 s1 的第 i 个元素贡献，
                    // 或由 s1 的第 i 个元素贡献，且 s1 的前 i-1 个元素和 s2 的前 j 个元素能交错组成 s3 的前 i+j-1 个元素
                    f[i][j] = f[i - 1][j] && s1.charAt(i - 1) == s3.charAt(p);
                if (j > 0)// 防越界
                    // s3 的第 i+j 个元素由 s1 的第 i 个元素贡献，
                    // 或由 s2 的第 j 个元素贡献，且 s1 的前 i 个元素和 s2 的前 j-1 个元素能交错组成 s3 的前 i+j-1 个元素
                    f[i][j] = f[i][j] || (f[i][j - 1] && s2.charAt(j - 1) == s3.charAt(p));
            }
        }
        return f[n][m];
    }

    // 方法一：（自己写的）动态规划-时间复杂度：O(nm)，空间复杂度：O(m)。
    // 代码较长，但逻辑更清楚
    public boolean isInterleave11(String s1, String s2, String s3) {
        if (s1.length() + s2.length() != s3.length())
            return false;

        boolean dp[][] = new boolean[s1.length() + 1][s2.length() + 1];
        dp[0][0] = true;

        // 处理第0行（s3 的第 0+j 个元素由 s2 的第 j 个元素贡献）
        for (int j = 1; j <= s2.length(); j++) {
            char c2 = s2.charAt(j - 1);
            char c3 = s3.charAt(j - 1);
            if (c2 == c3)
                dp[0][j] = dp[0][j - 1];
        }

        for (int i = 1; i <= s1.length(); i++) {
            // 处理第1行开始的第0列（s3 的第 i+0 个元素由 s1 的第 i 个元素贡献）
            char c1 = s1.charAt(i - 1);
            char c3 = s3.charAt(i - 1);
            if (c1 == c3)
                dp[i][0] = dp[i - 1][0];

            for (int j = 1; j <= s2.length(); j++) {
                char c2 = s2.charAt(j - 1);
                c3 = s3.charAt(i + j - 1);
                // s3 的第 i+j 个元素由 s1 的第 i 个元素贡献，则需要看其对应的上一个状态
                // 即 s1 的前 i-1 个元素和 s2 的前 j 个元素能交错组成 s3 的前 i+j-1 个元素
                if (c1 == c3)
                    dp[i][j] = dp[i][j] | dp[i - 1][j];
                // s3 的第 i+j 个元素由 s2 的第 j 个元素贡献，则需要看其对应的上一个状态
                // 即 s1 的前 i 个元素和 s2 的前 j-1 个元素能交错组成 s3 的前 i+j-1 个元素
                if (c2 == c3)
                    dp[i][j] = dp[i][j] | dp[i][j - 1];
            }
        }
        return dp[s1.length()][s2.length()];
    }

    // 方法二：递归 回溯 剪枝（双指针的改进，双指针无法处理相同字符，这时需要分叉再剪枝）
    // 尝试过，超时

    // 剑指Offer II 097.子序列的数目（主站115.）
    // 给定一个字符串 s 和一个字符串 t ，计算在 s 的子序列中 t 出现的个数。
    // 字符串的一个 子序列 是指，通过删除一些（也可以不删除）字符且不干扰剩余字符相对位置所组成的新字符串。
    // （例如，"ACE" 是 "ABCDE" 的一个子序列，而 "AEC" 不是）
    // 题目数据保证答案符合 32 位带符号整数范围。

    // 方法一：动态规划-时空复杂度：O(mn)
    // 自己的理解：计算在 s 的子序列中 t 出现的个数，即是有几种方式使得，删除s中的字符变成t
    // dp[i][j] 表示在 s[前i个字符] 的子序列中， t[前j个字符] 出现的个数。
    public int numDistinct(String s, String t) {
        int m = s.length(), n = t.length();
        if (m < n)
            return 0;

        int[][] dp = new int[m + 1][n + 1];
        for (int i = 0; i <= m; i++) // 初始化，方便后续计算
            dp[i][0] = 1;

        for (int i = 1; i <= m; i++) {
            char sChar = s.charAt(i - 1);
            for (int j = 1; j <= n; j++) {
                char tChar = t.charAt(j - 1);
                if (sChar == tChar)// 字符相等，
                    dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];// 左上dp+上dp，保留当前字符 或者 删除当前字符
                else
                    dp[i][j] = dp[i - 1][j];// 继承上dp，即删除当前字符

            }
        }
        return dp[m][n];
    }

    // 方法一：（自己写的）动态规划 + 滚动数组-时空复杂度：O(mn)
    public int numDistinct11(String s, String t) {
        int[] dp = new int[t.length() + 1];
        dp[0] = 1;
        for (int i = 1; i <= s.length(); i++) {
            char cs = s.charAt(i - 1);
            // 注意遍历顺序
            for (int j = t.length(); j >= 1; j--) {
                char ct = t.charAt(j - 1);
                if (cs == ct)
                    dp[j] += dp[j - 1];
            }
        }
        return dp[t.length()];
    }

    // 方法二：递归 回溯

    // 剑指Offer II 098.路径的数目（主站62. hot100有）
    // 一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为 “Start” ）。
    // 机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为 “Finish” ）。
    // 问总共有多少条不同的路径？

    // 方法一：动态规划-时间复杂度：O(mn)，空间复杂度：O(mn)
    // 我们用 f(i, j)表示从左上角走到 (i, j) 的路径数量，其中 i 和 j 的范围分别是 [0, m) 和 [0, n)。
    // 由于我们每一步只能从向下或者向右移动一步，因此要想走到 (i, j)，如果向下走一步，那么会从 (i-1, j) 走过来；
    // 如果向右走一步，那么会从 (i, j-1) 走过来。因此我们可以写出动态规划转移方程：
    // f(i, j) = f(i-1, j) + f(i, j-1)
    public int uniquePaths(int m, int n) {
        int[] dp = new int[n];
        Arrays.fill(dp, 1);
        dp[0] = 1;
        for (int i = 1; i < m; i++)
            for (int j = 1; j < n; j++)
                dp[j] = dp[j - 1] + dp[j];

        return dp[n - 1];
    }

    // 方法二：组合数学-时间复杂度：O(m)，空间复杂度：O(1)
    // C(m-1, m-1+n-1)
    public int uniquePaths2(int m, int n) {
        long ans = 1;
        for (int x = n, y = 1; y < m; ++x, ++y)
            ans = ans * x / y;

        return (int) ans;
    }

    // 剑指Offer II 099.最小路径之和（主站64. hot100有）
    // 给定一个包含非负整数的 m x n 网格 grid ，请找出一条从左上角到右下角的路径，使得路径上的数字总和为最小。
    // 说明：一个机器人每次只能向下或者向右移动一步。

    // 方法一：动态规划（优化内存开销，每次只存储上一行的 dp 值）-时间复杂度：O(mn)，空间复杂度：O(n)
    public int minPathSum2(int[][] grid) {
        if (grid == null || grid[0].length == 0)
            return 0;

        int rows = grid.length, columns = grid[0].length;
        int[] dp = new int[columns];
        dp[0] = grid[0][0];

        // 计算第一行
        for (int column = 1; column < columns; column++)
            dp[column] = dp[column - 1] + grid[0][column];

        for (int i = 1; i < rows; i++) {
            dp[0] = dp[0] + grid[i][0];// 单独处理每行第一个元素
            for (int j = 1; j < columns; j++)
                dp[j] = Math.min(dp[j - 1], dp[j]) + grid[i][j];
        }

        return dp[columns - 1];
    }

    // 方法二：dfs（自己的lowb想法）
    // 直接从头到尾，中间点会有大量重复计算，考虑记忆化搜索，每个中间点都存在最优，将dfs每步变小，每步只向右向左，即动态规划

    // 剑指Offer II 100.三角形中最小路径之和（主站120.）
    // 给定一个三角形 triangle ，找出自顶向下的最小路径和。
    // 每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
    // 也就是说，如果正位于当前行的下标 i ，那么下一步可以移动到下一行的下标 i 或 i + 1 。

    // 方法一：动态规划 + 空间优化-时间复杂度：O(n^2)，空间复杂度：O(n)
    // 空间优化时，注意计算的先后顺序
    public int minimumTotal(List<List<Integer>> triangle) {
        int n = triangle.size();
        int[] f = new int[n];
        f[0] = triangle.get(0).get(0);
        for (int i = 1; i < n; ++i) {// 第i层
            f[i] = f[i - 1] + triangle.get(i).get(i);// 特殊处理末尾
            for (int j = i - 1; j > 0; --j) // 除去特殊处理的尾首，每层从后往前开始
                f[j] = Math.min(f[j - 1], f[j]) + triangle.get(i).get(j);
            f[0] += triangle.get(i).get(0);// 特殊处理起始
        }
        int minTotal = Integer.MAX_VALUE;
        for (int i = 0; i < n; ++i) // 末层最小值即为最终答案
            minTotal = Math.min(minTotal, f[i]);
        return minTotal;
        // return Arrays.stream(f).min().getAsInt();
    }

    // 方法二：本题还有一些其它的动态规划方法，例如：从三角形的底部开始转移，到顶部结束；直接在给定的三角形数组上进行状态转移，不使用额外的空间。
    public int minimumTotal2(List<List<Integer>> triangle) {
        int m = triangle.size();
        for (int i = 1; i < m; i++) {
            List<Integer> prev = triangle.get(i - 1);
            List<Integer> list = triangle.get(i);
            int n = list.size();
            list.set(n - 1, list.get(n - 1) + prev.get(n - 2));
            for (int j = n - 2; j >= 1; j--)
                list.set(j, list.get(j) + Math.min(prev.get(j - 1), prev.get(j)));
            list.set(0, list.get(0) + prev.get(0));
        }
        return Collections.min(triangle.get(m - 1));
    }

    // 剑指 Offer II 101. 分割等和子集（主站416.）
    // 给定一个非空的正整数数组 nums ，请判断能否将这些数字分成元素和相等的两部分。
    // 提示：
    // 1 <= nums.length <= 200
    // 1 <= nums[i] <= 100
    // 本题是经典的「NP 完全问题」
    // 类似于传统的「0-1 背包问题」

    // 方法一：动态规划
    public boolean canPartition(int[] nums) {
        int n = nums.length;
        if (n < 2)
            return false;

        int sum = 0, maxNum = 0;
        for (int num : nums) {
            sum += num;
            maxNum = Math.max(maxNum, num);
        }

        if (sum % 2 != 0)
            return false;

        int target = sum / 2;
        if (maxNum > target)
            return false;

        boolean[] dp = new boolean[target + 1];
        dp[0] = true;
        for (int num : nums)
            for (int i = target; i >= num; i--)
                dp[i] |= dp[i - num];
        return dp[target];
    }

    // 剑指Offer II 102.加减的目标值（主站494.）
    // 给定一个正整数数组 nums 和一个整数 target 。
    // 向数组中的每个整数前添加 '+' 或 '-' ，然后串联起所有整数，可以构造一个 表达式 ：
    // 例如，nums = [2, 1] ，可以在 2 之前添加 '+' ，在 1 之前添加 '-' ，然后串联起来得到表达式 "+2-1" 。
    // 返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

    // 方法一：回溯-时间复杂度：O(2^n)，空间复杂度：O(n)

    // 方法二：动态规划-时间复杂度：O(n×(sum−target))，空间复杂度：O(sum−target)
    // 记数组的元素和为 um，添加 - 号的元素之和为 neg，则其余添加 + 的元素之和为 sum−neg，
    // 得到的表达式的结果为 (sum−neg) − neg = sum − 2⋅neg = target
    // neg = (sum−target)/2
    // 由于数组 nums 中的元素都是非负整数，neg 也必须是非负整数，所以上式成立的前提是 sum−target 是非负偶数。
    // 若不符合该条件可直接返回 0。

    // 此时问题转化成在数组 nums 中选取若干元素，使得这些元素之和等于 neg，计算选取元素的方案数。
    // 这是典型的背包问题。我们可以使用动态规划的方法求解。
    public int findTargetSumWays2(int[] nums, int target) {
        int sum = 0;
        for (int num : nums)
            sum += num;

        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0)
            return 0;

        // 定义数组 dp，其中 dp[i][j] 表示在数组 nums 的前 i 个数中选取元素，使得这些元素之和等于 j 的方案数。
        // 假设数组 nums 的长度为 n，则最终答案为dp[n][neg]。
        int n = nums.length, neg = diff / 2;// 问题转化后的目标和
        int[][] dp = new int[n + 1][neg + 1];// 默认初始化为0
        dp[0][0] = 1;
        for (int i = 1; i <= n; i++) {// 第i个数
            int num = nums[i - 1];
            for (int j = 0; j <= neg; j++) {// 和为j
                dp[i][j] = dp[i - 1][j];// 继承上一状态
                if (num <= j)// 第i个数小于j，可以放入背包，否则不放入
                    dp[i][j] += dp[i - 1][j - num];

            }
        }
        return dp[n][neg];
    }

    // 方法三：动态规划 + 空间优化-时间复杂度：O(n×(sum−target))，空间复杂度：O(sum−target)
    // dp[i] 表示目标值的组合数
    public int findTargetSumWays3(int[] nums, int target) {
        int sum = 0;
        for (int num : nums)
            sum += num;

        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0)
            return 0;

        int neg = diff / 2;// 问题转化后的目标和
        int[] dp = new int[neg + 1];// 默认初始化为0
        dp[0] = 1;
        for (int num : nums)// 第i个数
            for (int j = neg; j >= num; j--) // 注意顺序，从后往前（第i个数小于j才放入背包）
                dp[j] += dp[j - num];

        return dp[neg];
    }

    // 剑指Offer II 103.最少的硬币数目（类似完全背包 主站322. hot100有）
    // 给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。
    // 如果没有任何一种硬币组合能组成总金额，返回 -1。
    // 你可以认为每种硬币的数量是无限的。

    // 方法一：动态规划-时间复杂度：O(Sn)，其中 S 是金额，n 是面额数。
    // 我们一共需要计算 O(S) 个状态，S 为题目所给的总金额。对于每个状态，每次需要枚举 n 个面额来转移状态。
    // 空间复杂度：O(S)。
    // dp的下标表示要凑出面额
    // 先确定末尾（要凑出的面额）
    public int coinChange(int[] coins, int amount) {

        int max = amount + 1;// 最终答案的最大值为amount（全部由面额1组成）
        int[] dp = new int[amount + 1];// dp的下标表示要凑出面额
        Arrays.fill(dp, max);// 初始化为amount + 1 方便后面比较取小，且只要最终答案的值大于max，一定无法凑出该面额
        dp[0] = 0;// 方便后续凑

        for (int i = 1; i <= amount; i++) // 期望凑到面额i，amount=0则直接跳出循环返回-1
            for (int coin : coins) // 尝试使用各种面额凑到面额i，面额i =（总额i-该面额）+ 该面额
                if (coin <= i)
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);

        return dp[amount] >= max ? -1 : dp[amount];
    }

    // 方法一：动态规划-时间复杂度：O(Sn)，空间复杂度：O(S)。
    // 先确定起始（从该面额开始凑）
    public int coinChange11(int[] coins, int amount) {

        int max = amount + 1;// 最终答案的最大值为amount（全部由面额1组成）
        int[] dp = new int[amount + 1];// dp的下标表示要凑出面额
        Arrays.fill(dp, max);// 初始化为amount + 1 方便后面比较取小，且只要最终答案的值大于max，一定无法凑出该面额
        dp[0] = 0;// 方便后续凑

        for (int i = 0; i <= amount; i++) // 从总额i开始凑，amount=0则直接跳出循环返回-1
            for (int coin : coins) // （总额i-该面额）+ 各面额
                if (coin <= amount && i + coin <= amount)// 样例有整型最大值的面额，醉了
                    dp[i + coin] = Math.min(dp[i + coin], dp[i] + 1);

        return dp[amount] >= max ? -1 : dp[amount];
    }

    // 方法一：（自己写的）动态规划-时间复杂度：O(Sn)，空间复杂度：O(S)。
    // 循环内外层交换，依次对各种币进行操作
    public int coinChange111(int[] coins, int amount) {
        int[] dp = new int[amount + 1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int coin : coins) {
            if (coin > amount)
                continue;
            for (int j = 0; j < amount; j++) {
                if (dp[j] == Integer.MAX_VALUE)
                    continue;
                if (j + coin <= amount)
                    dp[j + coin] = Math.min(dp[j + coin], dp[j] + 1);
            }
        }
        if (dp[amount] == Integer.MAX_VALUE)
            return -1;
        else
            return dp[amount];
    }

    // 方法三：dfs 记忆化搜索-时间复杂度：O(Sn)，其中 S 是金额，n 是面额数。空间复杂度：O(S)
    // 为了避免重复的计算，我们将每个子问题的答案存在一个数组中进行记忆化，如果下次还要计算这个问题的值直接从数组中取出返回即可，
    // 这样能保证每个子问题最多只被计算一次。
    public int coinChange2(int[] coins, int amount) {
        if (amount < 1)
            return 0;

        return coinChange(coins, amount, new int[amount]);
    }

    private int coinChange(int[] coins, int rem, int[] count) {
        if (rem < 0)
            return -1;

        if (rem == 0)
            return 0;

        if (count[rem - 1] != 0)// 通过数组避免一些重复计算，对于每个不同的问题（不同面额的币），count都不相同
            return count[rem - 1];

        int min = Integer.MAX_VALUE;
        for (int coin : coins) {
            int res = coinChange(coins, rem - coin, count); // 选择一种币，分解成几个子问题
            if (res >= 0 && res < min) // res=-1则表示无法凑出
                min = 1 + res;

        }
        count[rem - 1] = (min == Integer.MAX_VALUE) ? -1 : min;// 更新数组
        return count[rem - 1];
    }

    // 剑指Offer II 104.排列的数目（主站377.）
    // 给定一个由 不同 正整数组成的数组 nums ，和一个目标整数 target 。请从 nums 中找出并返回总和为 target 的元素组合的个数。
    // 数组中的数字可以在一次排列中出现任意次，但是顺序不同的序列被视作不同的组合。
    // 题目数据保证答案符合 32 位整数范围。

    // 方法一：动态规划-时间复杂度：O(target×n)，空间复杂度：O(target)
    // 先确定末尾(target)
    public int combinationSum4(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;// 方便后续计算
        for (int i = 1; i <= target; i++)
            for (int num : nums)
                if (i - num >= 0)
                    dp[i] += dp[i - num];

        return dp[target];
    }

    // 方法二：动态规划-时间复杂度：O(target×n)，空间复杂度：O(target)
    // 先确定起始(i + num = target)
    public int combinationSum42(int[] nums, int target) {
        int[] dp = new int[target + 1];
        dp[0] = 1;// 方便后续计算
        for (int i = 0; i <= target; i++)
            for (int num : nums)
                if (i + num <= target)
                    dp[i + num] += dp[i];

        return dp[target];
    }

    // 剑指Offer II 107.矩阵中的距离（主站542.）
    // 给定一个由 0 和 1 组成的矩阵 mat ，请输出一个大小相同的矩阵，其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。
    // 两个相邻元素间的距离为 1 。
    // 提示：
    // m == mat.length
    // n == mat[i].length
    // 1 <= m, n <= 104
    // 1 <= m * n <= 104
    // mat[i][j] is either 0 or 1.
    // mat 中至少有一个 0 

    // 使用 dfs 貌似不行（路径抵达的先后问题，无法找到最小值）

    // 方法一：动态规划-时间复杂度：O(rc)，空间复杂度：O(1)
    // 为避免重复计算，更新dist时只考虑：（两趟即可，如何证明呢？）
    // 水平向左移动 和 竖直向上移动；
    // 水平向右移动 和 竖直向下移动。
    public int[][] updateMatrix(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[][] dist = new int[m][n];
        for (int i = 0; i < m; ++i)// 初始化动态规划的数组，所有的距离值都设置为一个很大的数
            Arrays.fill(dist[i], Integer.MAX_VALUE / 2);

        // 如果 (i, j) 的元素为 0，那么距离为 0
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j)
                if (matrix[i][j] == 0)
                    dist[i][j] = 0;

        // 只有 水平向左移动 和 竖直向上移动，注意动态规划的计算顺序
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j) {
                if (i - 1 >= 0)// 防越界
                    dist[i][j] = Math.min(dist[i][j], dist[i - 1][j] + 1);// 尝试通过上方更新dist
                if (j - 1 >= 0)
                    dist[i][j] = Math.min(dist[i][j], dist[i][j - 1] + 1);// 左方
            }

        // 只有 水平向右移动 和 竖直向下移动，注意动态规划的计算顺序（与上面方向相反）
        for (int i = m - 1; i >= 0; --i)
            for (int j = n - 1; j >= 0; --j) {
                if (i + 1 < m)
                    dist[i][j] = Math.min(dist[i][j], dist[i + 1][j] + 1);// 下方
                if (j + 1 < n)
                    dist[i][j] = Math.min(dist[i][j], dist[i][j + 1] + 1);// 右方
            }

        return dist;
    }

    // 方法二：bfs-时间复杂度：O(rc)，空间复杂度：O(rc)
    // 多个源点出发的最短路，dfs 貌似不行
    public int[][] updateMatrix2(int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[][] dist = new int[m][n];// 答案数组
        int[][] dirs = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };// 四个方向
        boolean[][] seen = new boolean[m][n];// 标记数组，防止重复遍历（第一次更新dist时一定是最短路）
        Queue<int[]> queue = new LinkedList<int[]>();// 辅助队列

        // 将所有的 0 添加进初始队列中
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j)
                if (matrix[i][j] == 0) {
                    queue.offer(new int[] { i, j });
                    seen[i][j] = true;
                }

        // bfs
        while (!queue.isEmpty()) {
            int[] cell = queue.poll();
            int i = cell[0], j = cell[1];
            for (int d = 0; d < 4; ++d) {// 遍历四个方向，更新相邻节点dist
                int ni = i + dirs[d][0];
                int nj = j + dirs[d][1];
                if (ni >= 0 && ni < m && nj >= 0 && nj < n && !seen[ni][nj]) {// 防越界，未遍历
                    dist[ni][nj] = dist[i][j] + 1;
                    queue.offer(new int[] { ni, nj });
                    seen[ni][nj] = true;
                }
            }
        }

        return dist;
    }

    // 剑指Offer II 112.最长递增路径（主站329.）
    // 给定一个 m x n 整数矩阵 matrix ，找出其中 最长递增路径 的长度。
    // 对于每个单元格，你可以往上，下，左，右四个方向移动。 不能 在 对角线 方向上移动或移动到 边界外（即不允许环绕）。
    // 改题不用visited数组，由于数值大小限制，一定不会成环

    // 方法一：记忆化 dfs-时间复杂度：O(mn)，空间复杂度：O(mn)
    // 一个节点的 dfs 一定能得到多个节点的最终答案
    // 传统的动态规划，几趟遍历都不一定能得到最终答案
    public int[][] dirs = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
    public int rows, columns;
    int[][] memo;// 缓存矩阵，已经计算过的单元格的结果存储到缓存矩阵中，默认为0，表示还未计算过

    public int longestIncreasingPath(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
            return 0;

        rows = matrix.length;
        columns = matrix[0].length;
        memo = new int[rows][columns];
        int ans = 0;
        for (int i = 0; i < rows; ++i)
            for (int j = 0; j < columns; ++j)
                ans = Math.max(ans, dfs(matrix, i, j));

        return ans;
    }

    // 寻找以(row, column)为终点的最长递增路径
    public int dfs(int[][] matrix, int row, int column) {
        if (memo[row][column] != 0)// 使用memo数组避免重复计算
            return memo[row][column];

        memo[row][column] = 1;// 默认步长为1
        for (int[] dir : dirs) {
            int newRow = row + dir[0], newColumn = column + dir[1];// 根据dir数组生成的相邻节点的坐标
            if (newRow >= 0 && newRow < rows && newColumn >= 0 && newColumn < columns// 防越界
                    && matrix[newRow][newColumn] > matrix[row][column])// 相邻节点更小
                memo[row][column] = Math.max(memo[row][column], dfs(matrix, newRow, newColumn) + 1);
        }
        return memo[row][column];
    }

    // 方法二：拓扑排序 bfs（动态规划）-时间复杂度：O(mn)，空间复杂度：O(mn)
    // 将矩阵看成一个有向图，计算每个单元格对应的出度，即有多少条边从该单元格出发。
    // 对于作为边界条件的单元格，该单元格的值比所有的相邻单元格的值都要大，因此作为边界条件的单元格的出度都是 0。
    // 1→2→3
    public int longestIncreasingPath2(int[][] matrix) {
        if (matrix == null || matrix.length == 0 || matrix[0].length == 0)
            return 0;

        int[][] dirs = { { -1, 0 }, { 1, 0 }, { 0, -1 }, { 0, 1 } };
        int rows, columns;
        rows = matrix.length;
        columns = matrix[0].length;
        int[][] outdegrees = new int[rows][columns];// 出度，默认为0
        for (int i = 0; i < rows; ++i)// 计算每个节点的出度
            for (int j = 0; j < columns; ++j)
                for (int[] dir : dirs) {
                    int newRow = i + dir[0], newColumn = j + dir[1];
                    if (newRow >= 0 && newRow < rows && newColumn >= 0 && newColumn < columns// 防越界
                            && matrix[newRow][newColumn] > matrix[i][j])// 相邻节点更大
                        ++outdegrees[i][j];
                }

        Queue<int[]> queue = new LinkedList<int[]>();
        for (int i = 0; i < rows; ++i)// 所有出度为0的点入队
            for (int j = 0; j < columns; ++j)
                if (outdegrees[i][j] == 0)
                    queue.offer(new int[] { i, j });

        int ans = 0;
        while (!queue.isEmpty()) {
            ++ans;
            int size = queue.size();
            for (int i = 0; i < size; ++i) {// 每趟遍历出度为0的节点
                int[] cell = queue.poll();
                int row = cell[0], column = cell[1];
                for (int[] dir : dirs) {
                    int newRow = row + dir[0], newColumn = column + dir[1];
                    if (newRow >= 0 && newRow < rows && newColumn >= 0 && newColumn < columns// 防越界
                            && matrix[newRow][newColumn] < matrix[row][column]) // 相邻节点更小
                        if (--outdegrees[newRow][newColumn] == 0) // 更新出度后，出度为0的节点入队
                            queue.offer(new int[] { newRow, newColumn });

                }
            }
        }
        return ans;
    }
}

// 回溯
class BackTrackOfferII {
    // 剑指Offer II 086.分割回文子字符串
    // 剑指Offer II 079.所有子集

    // 剑指Offer II 080.含有 k 个元素的组合（主站77.）
    // 给定两个整数 n 和 k，返回 1 ... n 中所有可能的 k 个数的组合。

    // 方法一：递归实现组合型枚举-时间复杂度：O((n, k)×k)，空间复杂度：O(n)
    List<Integer> temp = new ArrayList<Integer>();
    List<List<Integer>> ans = new ArrayList<List<Integer>>();

    public List<List<Integer>> combine(int n, int k) {
        dfs(1, n, k);
        return ans;
    }

    public void dfs(int cur, int n, int k) {
        // 剪枝：temp 长度加上区间 [cur, n] 的长度小于 k，不可能构造出长度为 k 的 temp、
        // cur不可能大于n，即不可能将大于n的数加入到temp当中
        if (temp.size() + (n - cur + 1) < k)
            return;

        // 记录合法的答案
        if (temp.size() == k) {
            ans.add(new ArrayList<Integer>(temp));
            return;
        }
        // 考虑选择当前位置
        temp.add(cur);
        dfs(cur + 1, n, k);
        temp.remove(temp.size() - 1);// 记得还原
        // 考虑不选择当前位置
        dfs(cur + 1, n, k);
    }

    // 方法二：非递归（字典序法）实现组合型枚举-时间复杂度：O((n, k)×k)，空间复杂度：O(k)
    // 43[2][1] 0011 2, 1
    // 4[3]2[1] 0101 3, 1
    // 如何找到它的字典序中的下一个数字 next(x)：

    // 规则一：x 的最低位为 1，这种情况下（交换一次），
    // 如果末尾由 t 个连续的 1，我们直接将倒数第 t 位的 1 和倒数第 t + 1 位的 0 替换，就可以得到 next(x)。
    // （即末尾连续最高位的1与其左边的0交换位置）
    // 如 0011→0101，0101→0110，1001→1010，1001111→1010111。

    // 规则二：x 的最低位为 0，这种情况下（交换一次，移动一次），
    // 末尾有 t 个连续的 0，而这 t 个连续的 0 之前有 m 个连续的 1，
    // 我们可以将倒数第 t + m 位置的 1 和倒数第 t + m + 1 位的 0 对换，
    // 然后把倒数第 t + 1 位到倒数第 t + m - 1 位的 1 移动到最低位。
    // （即末尾连续最高位的1与其左边的0交换位置，末尾连续最低位的1到之前0交换到的位置的所有1移动到最低位）
    // 如 0110→1001，1010→1100，1011100→1100011。
    public List<List<Integer>> combine2(int n, int k) {
        List<Integer> temp = new ArrayList<Integer>();
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        // 初始化
        // 将 temp 中 [0, k - 1] 每个位置 i 设置为 i + 1，即 [0, k - 1] 存 [1, k]
        // 末尾加一位 n + 1 作为哨兵
        for (int i = 1; i <= k; ++i)
            temp.add(i);
        temp.add(n + 1);

        int j = 0;

        // 字典序法的实现非常巧妙，化繁为简，一步到位（不在二进制层面上处理，直接处理数组元素）
        while (j < k) {
            ans.add(new ArrayList<Integer>(temp.subList(0, k)));
            j = 0;
            // 寻找第一个 temp[j] + 1 != temp[j + 1] 的位置 t
            // 我们需要把 [0, t - 1] 区间内的每个位置重置成 [1, t]
            while (j < k && temp.get(j) + 1 == temp.get(j + 1)) {
                temp.set(j, j + 1);
                ++j;
            }
            // j 是第一个 temp[j] + 1 != temp[j + 1] 的位置
            temp.set(j, temp.get(j) + 1);
        }
        return ans;
    }

    // 方法二：（自己写的）“直译” 字典序法-时间复杂度：O((n, k)×k)，空间复杂度：O(n)
    public List<List<Integer>> combine22(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();
        // 0 表示不选取该值，1 表示选取该值
        int[] array = new int[n];
        for (int i = 0; i < k; i++)
            array[i] = 1;

        int[] next = array;
        while (next != null) {
            List<Integer> ans = new ArrayList<>();
            for (int i = 0; i < n; i++)
                if (next[i] == 1)
                    ans.add(i + 1);
            res.add(ans);
            next = getNext(next);
        }
        return res;
    }

    // 找到array在字典序中的下一个数字（组合）
    private int[] getNext(int[] array) {
        int n = array.length;
        int[] ans = new int[n];
        int oneStart = 0, oneEnd = 0;
        // 情况一：最低位为 1
        // （即末尾连续最高位的1与其左边的0交换位置）
        if (array[0] == 0) {
            // 找到 oneEnd
            for (int i = oneEnd; i < n; i++) {
                if (array[i] == 0)
                    break;
                oneEnd = i;
            }

            // array在字典序中已经没有下一个数字（组合）
            if (oneEnd == n - 1)
                return null;

            // 交换一次
            for (int i = 0; i < n; i++)
                ans[i] = array[i];
            ans[oneEnd] = 0;
            ans[oneEnd + 1] = 1;
            // 情况二：最低位为 0
            // （即末尾连续最高位的1与其左边的0交换位置，末尾连续最低位的1到之前0交换到的位置的所有1移动到最低位）
        } else {
            // 找到 oneStart
            for (int i = 0; i < n; i++)
                if (array[i] == 1) {
                    oneStart = i;
                    break;
                }

            // 找到 oneEnd
            oneEnd = oneStart;
            for (int i = oneEnd; i < n; i++) {
                if (array[i] == 0)
                    break;
                oneEnd = i;
            }

            // array在字典序中已经没有下一个数字（组合）
            if (oneEnd == n - 1)
                return null;

            // 移动1
            for (int i = 0; i < oneEnd - oneStart; i++)
                ans[i] = 1;

            // 交换一次
            ans[oneEnd + 1] = 1;

            // 复制剩余部分
            for (int i = oneEnd + 2; i < n; i++)
                ans[i] = array[i];
        }
        return ans;
    }

    // 方法三：（自己写的）位运算-时间复杂度：O((n, k)×k)，空间复杂度：O(2^n)
    // 先dp计算每个排列组合对应的二进制的1的个数，若个数==k，则转化为对应排列组合加入最终答案
    public List<List<Integer>> combine3(int n, int k) {
        List<List<Integer>> res = new ArrayList<>();

        // dp计算每个排列组合对应的二进制的1的个数
        int[] countOne = new int[(int) Math.pow(2, n)];
        for (int i = 1; i < countOne.length; i++) {
            countOne[i] = countOne[i / 2];
            if ((i & 1) == 1)
                countOne[i]++;
        }

        // 若个数==k，则转化为对应排列组合加入最终答案
        for (int i = 0; i < countOne.length; i++) {
            if (countOne[i] != k)
                continue;

            List<Integer> ans = new ArrayList<>();
            for (int j = 0; j < n; j++)
                if (((i >> j) & 1) == 1)
                    ans.add(j + 1);

            res.add(ans);
        }
        return res;
    }

    // 剑指Offer II 081.允许重复选择元素的组合（主站39. hot100有）
    // 给定一个无重复元素的正整数数组 candidates 和一个正整数 target ，
    // 找出 candidates 中所有可以使数字和为目标数 target 的唯一组合。
    // candidates 中的数字可以无限制重复被选取。如果至少一个所选数字数量不同，则两种组合是不同的。 
    // 对于给定的输入，保证和为 target 的唯一组合数少于 150 个。
    // 提示：
    // 1 <= candidates.length <= 30
    // 1 <= candidates[i] <= 200
    // 「candidate 中的每个元素都是独一无二的」。
    // 1 <= target <= 500
    // 与主站322.零钱兑换不同的是，本题需要返回不重复的具体情况（完整路径），采用搜索回溯（递归）更好，而不是动态规划

    // 方法一：搜索回溯-时间复杂度：O(S)，其中 S 为所有可行解的长度之和，空间复杂度：O(target)
    // 使用一般的递归会有重复解出现，此时去重有些麻烦
    // 不重复且不遗漏地找到所有可行解：
    // 递归：1. 跳过某数（后续再也不选择该数），2. 选择某数（后续仍可选择该数），combine中元素排列顺序同candidates[]
    // List<List<Integer>> ans = new ArrayList<List<Integer>>();
    List<Integer> combine = new ArrayList<>();// 一种情况

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        // 预处理，哪些值是可以组合出来的
        dfs(candidates, target, 0);
        return ans;
    }

    /**
     * 
     * @param candidates
     * @param target
     * @param idx        当前作决定的数的索引
     */
    public void dfs(int[] candidates, int target, int idx) {
        if (idx == candidates.length)
            return;

        if (target == 0) {
            ans.add(new ArrayList<>(combine));
            return;
        }
        // 直接跳过
        dfs(candidates, target, idx + 1);
        // 选择当前数
        if (target - candidates[idx] >= 0) {// 若加上candidates[idx]已超target，则跳过
            combine.add(candidates[idx]);
            dfs(candidates, target - candidates[idx], idx);// 可继续选择该数 candidates[idx]
            combine.remove(combine.size() - 1);// 递归结束后，去除添加的数（还原），调用其他递归
        }
    }

    // 剑指Offer II 082.含有重复元素集合的组合（主站40.）
    // 给定一个可能有重复数字的整数数组 candidates 和一个目标数 target ，找出 candidates 中所有可以使数字和为 target 的组合。
    // candidates 中的每个数字在每个组合中只能使用一次，解集不能包含重复的组合。 
    // 提示:
    // 1 <= candidates.length <= 100
    // 1 <= candidates[i] <= 50
    // 1 <= target <= 30

    // 方法一：回溯-时间复杂度：O(2^n×n)，空间复杂度：O(n)
    // 核心思想：对同一个数的选择，一次性选择1, 2, 3个,... <= 出现次数
    // List<List<Integer>> ans = new ArrayList<List<Integer>>();
    Map<Integer, Integer> freq = new HashMap<>();// 统计数组 candidates 中每个数出现的次数
    List<Integer> numPosition;// 同一个数对应一个位置，方便后续递归
    List<Integer> sequence = new ArrayList<Integer>();// 一种情况

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        for (int num : candidates)
            freq.put(num, freq.getOrDefault(num, 0) + 1);
        numPosition = new ArrayList<>(freq.keySet());// 顺序是candidates中的相对顺序
        dfs(0, target);
        return ans;
    }

    // 按位置递归
    public void dfs(int pos, int rest) {
        if (rest == 0) {
            ans.add(new ArrayList<Integer>(sequence));
            return;
        }

        if (pos == numPosition.size())// 已遍历完所有数（依旧没有匹配到正确答案）
            return;
        int num = numPosition.get(pos);// 当前位置对应的数

        dfs(pos + 1, rest);// 不选择当前位置数num，跳过

        if (num > rest)// 当前数大于rest
            return;
        int most = Math.min(rest / num, freq.get(num));// 根据 rest 和 当前位置对应数的出现次数 确定该数可以选择几次
        // 对于同一个数num，选择1, 2, 3个,... <= 出现次数 或 rest上限
        for (int i = 1; i <= most; ++i) {
            sequence.add(num);
            dfs(pos + 1, rest - i * num);
        }
        for (int i = 1; i <= most; ++i)// 移除当前位置的数num（可能多个）
            sequence.remove(sequence.size() - 1);
    }

    // 剑指Offer II 083.没有重复元素集合的全排列（主站46. hot100有）
    // 给定一个不含重复数字的整数数组 nums ，返回其 所有可能的全排列 。可以 按任意顺序 返回答案。
    // 提示：
    // 1 <= nums.length <= 6
    // -10 <= nums[i] <= 10
    // nums 中的所有整数 互不相同

    // 方法一：回溯-时间复杂度：O(n×n!)，空间复杂度：O(n)
    // 核心思想：依次交换位置
    List<List<Integer>> res = new ArrayList<List<Integer>>();
    List<Integer> output = new ArrayList<Integer>();
    int n;

    public List<List<Integer>> permute(int[] nums) {

        for (int num : nums)
            output.add(num);

        n = nums.length;
        backtrack(0);
        return res;
    }

    // 从左往右填到第 first 个位置
    public void backtrack(int first) {
        // 所有数都填完了
        if (first == n)
            res.add(new ArrayList<Integer>(output));

        for (int i = first; i < n; i++) {
            // 动态维护数组
            Collections.swap(output, first, i);// 第一次循环first = i，不交换
            // 继续递归填下一个数
            backtrack(first + 1);
            // 撤销操作
            Collections.swap(output, first, i);
        }
    }

    // 方法二：自己的想法dfs，还是需要判断哪些数用上了，bfs就不写了（也不太适合）
    // int n;
    // List<List<Integer>> res = new ArrayList<List<Integer>>();
    // 用数组标记是否已用也行
    Set<Integer> leftNums;// 没被用过的数字集合
    Set<Integer> existedNums;// 一种情况

    public List<List<Integer>> permute2(int[] nums) {
        n = nums.length;
        leftNums = new HashSet<>(nums.length);// 每次迭代顺序跟哈希值计算有关
        existedNums = new LinkedHashSet<>(nums.length); // 会记录放入顺序
        for (int num : nums)
            leftNums.add(num);
        dfs();
        return res;
    }

    public void dfs() {
        if (existedNums.size() == n)
            res.add(new ArrayList<Integer>(existedNums));

        List<Integer> leftList = new ArrayList<>(leftNums);// 返回迭代器后，容器不能更改
        Iterator<Integer> iter = leftList.iterator();// 顺序跟哈希值计算有关
        while (iter.hasNext()) {
            int selectedNum = iter.next();
            leftNums.remove(selectedNum);
            existedNums.add(selectedNum);
            dfs();
            // 还原
            leftNums.add(selectedNum);
            existedNums.remove(selectedNum);
        }
    }

    // 剑指Offer II 084.含有重复元素集合的全排列（主站47. ）
    // 给定一个可包含重复数字的整数集合 nums ，按任意顺序 返回它所有不重复的全排列。

    // 方法一：搜索回溯
    // 要解决重复问题，我们只要设定一个规则：
    // 保证在填第 idx 个数的时候重复数字只会被填入一次即可。
    boolean[] vis;

    public List<List<Integer>> permuteUnique(int[] nums) {
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        List<Integer> perm = new ArrayList<Integer>();// 符合要求的一种情况
        Arrays.sort(nums);// 使重复元素相邻
        vis = new boolean[nums.length];// 标记是否已经用过
        backtrack(nums, ans, 0, perm);
        return ans;
    }

    /**
     * 
     * @param nums
     * @param ans
     * @param idx  决定第idx个位置填什么数
     * @param perm
     */
    public void backtrack(int[] nums, List<List<Integer>> ans, int idx, List<Integer> perm) {
        if (idx == nums.length) {
            ans.add(new ArrayList<Integer>(perm));
            return;
        }
        for (int i = 0; i < nums.length; ++i) {
            if (vis[i]) // perm含有该数
                continue;
            // 当前数等于上一个数，即为重复数，且该重复数不在perm中（当前位置填该重复数的情况已经讨论过了）
            // 若该重复数在perm中，则前驱位置一定填的是该重复数（上一个数）
            if (i > 0 && nums[i] == nums[i - 1] && !vis[i - 1])
                continue;

            // 当前位置idx选择填入 nums[i]
            perm.add(nums[i]);
            vis[i] = true;
            backtrack(nums, ans, idx + 1, perm);

            // 当前位置idx选择不填入 nums[i]，尝试填入 nums[i+1]
            vis[i] = false;
            perm.remove(idx);
        }
    }

    // 方法一：（自己写的）搜索回溯
    // 感觉该方法判断重复使用元素更简单
    // int n;
    // List<List<Integer>> res = new ArrayList<>();
    List<Integer> ans84 = new ArrayList<>();
    Set<Integer> set = new HashSet<>();
    Map<Integer, Integer> numCount = new HashMap<>();

    public List<List<Integer>> permuteUnique11(int[] nums) {
        this.n = nums.length;
        for (int num : nums) {
            if (!numCount.containsKey(num)) {
                numCount.put(num, 1);
                set.add(num);
            } else
                numCount.put(num, numCount.get(num) + 1);

        }

        dfs(0);
        return res;
    }

    private void dfs(int index) {
        if (index == n) {
            res.add(new ArrayList<>(ans84));
            return;
        }

        for (int num : set) {
            if (numCount.get(num) > 0) {
                ans84.add(num);
                numCount.put(num, numCount.get(num) - 1);
                dfs(index + 1);
                ans84.remove(ans84.size() - 1);
                numCount.put(num, numCount.get(num) + 1);
            }
        }

    }

    // 剑指Offer II 085.生成匹配的括号（主站22. hot100有）
    // 正整数 n 代表生成括号的对数，请设计一个函数，用于能够生成所有可能的并且 有效的 括号组合。

    // 方法一：暴力法-时间复杂度：O(2^{2n}n)，空间复杂度：O(n)
    // 我们可以生成所有 2^2n 个 '(' 和 ')' 字符构成的序列，然后我们检查每一个是否有效即可。

    // 方法二：回溯法-时间复杂度：略，空间复杂度O(n)
    // 方法一还有改进的余地：我们可以只在序列仍然保持有效时才添加 '(' or ')'，而不是像 方法一 那样每次添加。
    // 我们可以通过跟踪到目前为止放置的左括号和右括号的数目来做到这一点，
    // 如果左括号数量不大于 nn，我们可以放一个左括号。如果右括号数量小于左括号的数量，我们可以放一个右括号。
    public List<String> generateParenthesis2(int n) {
        List<String> ans = new ArrayList<String>();
        backtrack(ans, new StringBuilder(), 0, 0, n);
        return ans;
    }

    /**
     * 
     * @param ans
     * @param cur
     * @param open  左括号数
     * @param close 右括号数
     * @param max
     */
    public void backtrack(List<String> ans, StringBuilder cur, int open, int close, int max) {
        if (cur.length() == max * 2) {// 长度满足条件
            ans.add(cur.toString());
            return;
        }

        // 还能填左括号
        if (open < max) {
            cur.append('(');
            backtrack(ans, cur, open + 1, close, max);
            cur.deleteCharAt(cur.length() - 1);// 递归后记得删除末尾括号
        }

        // 还能填右括号
        if (close < open) {
            cur.append(')');
            backtrack(ans, cur, open, close + 1, max);
            cur.deleteCharAt(cur.length() - 1);// 递归后记得删除末尾括号
        }
    }

    // 方法三：按括号序列的长度递归-时间复杂度：略，空间复杂度：略
    // 任何一个括号序列都一定是由 ( 开头，并且第一个 ( 一定有一个唯一与之对应的 )。
    // 这样一来，每一个括号序列可以用 (a)b 来表示，其中 a 与 b 分别是一个合法的括号序列（可以为空）。
    @SuppressWarnings("unchecked")
    List<String>[] cache = new ArrayList[9];// 记忆化缓存 含有n个括号的所有括号组合，提示：1 <= n <= 8

    public List<String> generate(int n) {
        if (cache[n] != null) // 已经计算过cache[n]
            return cache[n];

        // 计算cache[n]
        ArrayList<String> ans = new ArrayList<String>();
        if (n == 0)
            ans.add("");
        else
            for (int c = 0; c < n; ++c)
                for (String left : generate(c))
                    for (String right : generate(n - 1 - c))
                        ans.add("(" + left + ")" + right);

        cache[n] = ans;
        return ans;
    }

    public List<String> generateParenthesis3(int n) {
        return generate(n);
    }

    // 剑指Offer Il 087.复原IP（主站93.）
    // 给定一个只包含数字的字符串 s ，用以表示一个 IP 地址，返回所有可能从 s 获得的 有效 IP 地址 。你可以按任何顺序返回答案。
    // 有效 IP 地址 正好由四个整数（每个整数位于 0 到 255 之间组成，且不能含有前导 0），整数之间用 '.' 分隔。
    // 例如："0.1.2.201" 和 "192.168.1.1" 是 有效 IP 地址，
    // 但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效 IP 地址。
    // 提示：
    // 0 <= s.length <= 3000
    // s 仅由数字组成

    static final int SEG_COUNT = 4;
    List<String> ans87 = new ArrayList<String>();
    int[] segments = new int[SEG_COUNT];

    public List<String> restoreIpAddresses(String s) {
        if (s.length() > 12)
            return ans87;
        segments = new int[SEG_COUNT];
        dfs(s, 0, 0);
        return ans87;
    }

    public void dfs(String s, int segId, int segStart) {
        // 如果找到了 4 段 IP 地址并且遍历完了字符串，那么就是一种答案
        if (segId == SEG_COUNT) {// 已填满 4 段 IP 地址
            if (segStart == s.length()) {// 遍历完了字符串
                StringBuffer ipAddr = new StringBuffer();
                for (int i = 0; i < SEG_COUNT; ++i) {
                    ipAddr.append(segments[i]);
                    if (i != SEG_COUNT - 1)
                        ipAddr.append('.');
                }
                ans87.add(ipAddr.toString());
            }
            return;
        }

        // 如果还没有找到 4 段 IP 地址就已经遍历完了字符串，那么提前剪枝
        if (segStart == s.length())
            return;

        // 由于不能有前导零，如果当前数字为 0，那么这一段 IP 地址只能为 0
        if (s.charAt(segStart) == '0') {
            segments[segId] = 0;
            dfs(s, segId + 1, segStart + 1);
        }

        // 一般情况，枚举每一种可能性并递归
        int addr = 0;
        for (int segEnd = segStart; segEnd < s.length(); ++segEnd) {
            addr = addr * 10 + (s.charAt(segEnd) - '0');
            if (addr > 0 && addr <= 0xFF) {// 0 < addr <= 255
                segments[segId] = addr;// 覆盖上一种dfs
                dfs(s, segId + 1, segEnd + 1);
            } else
                break;
        }
    }

    // 剑指Offer II 102.加减的目标值
    // 剑指Offer II 110.所有路径
}

// 前缀和
class PrefixSumOfferII {
    // 剑指Offer II 008.和大于等于tr...
    // 剑指Offer II 010.和为k的子数组
    // 剑指Offer II 011. 0和1个数相...

    // 剑指Offer II 012.左右两边子数组的和相等（主站724.）
    // 给你一个整数数组 nums ，请计算数组的 中心下标 。
    // 数组 中心下标 是数组的一个下标，其左侧所有元素相加的和等于右侧所有元素相加的和。
    // 如果中心下标位于数组最左端，那么左侧数之和视为 0 ，因为在下标的左侧不存在元素。
    // 这一点对于中心下标位于数组最右端同样适用。
    // 如果数组有多个中心下标，应该返回 最靠近左边 的那一个。如果数组不存在中心下标，返回 -1 。
    // 提示：
    // 1 <= nums.length <= 104（无法根据窗口内值的大小移动左右指针，只能用用前缀和）
    // -1000 <= nums[i] <= 1000

    // 方法一：前缀和-时间复杂度：O(n)，空间复杂度：O(1)
    // 设计非常巧妙，第一次计算sum不记录前缀和（降低空间复杂度），第二次遍历利用sum初值为0，巧妙设置的边界条件-1时前缀和为0
    public int pivotIndex(int[] nums) {
        int total = Arrays.stream(nums).sum();
        int sum = 0;
        for (int i = 0; i < nums.length; ++i) {
            if (2 * sum + nums[i] == total)
                return i;

            sum += nums[i];
        }
        return -1;
    }

    // 剑指Offer II 013.二维子矩阵的和
    // 剑指Offer II 071.按权重生成随...

}

// 排序
class SortOfferII {
    // 剑指Offer II 007.数组中和为 0 的三个数（主站15. 三数之和 hot100有）
    // 给定一个包含 n 个整数的数组 nums，判断 nums 中是否存在三个元素 a ，b ，c ，使得 a + b + c = 0 ？
    // 请找出所有和为 0 且 不重复 的三元组。

    // 方法一：排序 + 双指针-时间复杂度：O(n^2)，空间复杂度：O(logn)
    // 不包含重复的三元组的关键：需要和上一次枚举的数不相同
    // 其他小优化点：各种if判断，类似提前剪枝
    public List<List<Integer>> threeSum(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<List<Integer>>();
        // a + b + c = 0
        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1])
                continue;

            // c 对应的指针初始指向数组的最右端
            int third = n - 1;
            int target = -nums[first]; // b + c = -a = target
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1])
                    continue;

                // 需要保证 b 的指针在 c 的指针的左侧
                while (second < third && nums[second] + nums[third] > target)
                    --third;

                // 如果指针重合，随着 b 后续的增加
                // 就不会有满足 a+b+c=0 并且 b<c 的 c 了，可以退出循环
                if (second == third)
                    break;

                // 所得即答案
                if (nums[second] + nums[third] == target) {
                    List<Integer> list = new ArrayList<Integer>();
                    list.add(nums[first]);
                    list.add(nums[second]);
                    list.add(nums[third]);
                    ans.add(list);
                }
            }
        }
        return ans;
    }

    // 方法二：排序 + 二分查找-时间复杂度：O(n^2)，空间复杂度：O(logn)
    public List<List<Integer>> threeSum2(int[] nums) {
        int n = nums.length;
        Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<List<Integer>>();

        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1])
                continue;

            // a + b + c = 0, b + c = -a = target
            int target = -nums[first];
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1])
                    continue;

                // 随着 b 后续的增加，不会有满足 a+b+c=0 并且 b<c 的 c 了，可以退出循环
                if (nums[second] > target / 2)
                    break;

                // 需要保证 b 的指针在 c 的指针的左侧
                int third = Arrays.binarySearch(nums, second + 1, n, target - nums[second]);

                // 没找到当前b对应的c，返回的是 -(应当插入的位置)-1
                if (third < 0)
                    continue;

                // third>=0，则找到了对应的c
                // 所得即答案
                List<Integer> list = Arrays.asList(nums[first], nums[second], nums[third]);// （一般还要放入实现类的构造器中，asList返回的是内部类）
                ans.add(list);
            }
        }
        return ans;
    }

    // 剑指Offer II 032.有效的变位词
    // 剑指Offer II 033.变位词组

    // 剑指Offer II 035.最小时间差（主站539.）
    // 给定一个 24 小时制（小时:分钟 "HH:MM"）的时间列表，找出列表中任意两个时间的最小时间差并以分钟数表示。

    // 方法一：排序 + 鸽巢原理-时间复杂度：O(min(n,C)logmin(n,C))，
    // 其中 n 是数组 timePoints 的长度，C=24×60=1440。
    // 空间复杂度：O(min(n,C))
    // 将 timePoints 排序后，最小时间差必然出现在 timePoints 的两个相邻时间，或者 timePoints 的两个首尾时间中。
    // 因此排序后遍历一遍 timePoints 即可得到最小时间差。
    public int findMinDifference(List<String> timePoints) {
        int n = timePoints.size();

        // 根据题意，一共有 24×60=1440 种不同的时间。
        // 由鸽巢原理可知，如果 timePoints 的长度超过 1440，那么必然会有两个相同的时间，此时可以直接返回 0。
        if (n > 1440)
            return 0;

        Collections.sort(timePoints);
        int ans = Integer.MAX_VALUE;
        int t0Minutes = getMinutes(timePoints.get(0));
        int preMinutes = t0Minutes;
        for (int i = 1; i < n; ++i) {
            int minutes = getMinutes(timePoints.get(i));
            ans = Math.min(ans, minutes - preMinutes); // 相邻时间的时间差
            preMinutes = minutes;
        }
        ans = Math.min(ans, t0Minutes + 1440 - preMinutes); // 首尾时间的时间差
        return ans;
    }

    // e.g. ["23:59","00:00"]
    public int getMinutes(String t) {
        return ((t.charAt(0) - '0') * 10 + (t.charAt(1) - '0')) * 60 + (t.charAt(3) - '0') * 10 + (t.charAt(4) - '0');
    }

    // 方法一：（自己写的）排序 + 鸽巢原理-时间复杂度：O(min(n,C)logmin(n,C))，空间复杂度：O(min(n,C))
    public int findMinDifference11(List<String> timePoints) {
        int n = timePoints.size();
        // 鸽巢原理
        if (n > 1440)
            return 0;

        int[] minutes = new int[n];
        for (int i = 0; i < n; i++) {
            String[] timePoint = timePoints.get(i).split(":");
            minutes[i] = Integer.valueOf(timePoint[0]) * 60 + Integer.valueOf(timePoint[1]);
        }

        Arrays.sort(minutes);
        int res = 24 * 60 - minutes[n - 1] + minutes[0];
        for (int i = 0; i < n - 1; i++)
            res = Math.min(res, minutes[i + 1] - minutes[i]);

        return res;
    }

    // 剑指Offer II 057.值和下标之差都在给定的范围内（主站220.）
    // 给你一个整数数组 nums 和两个整数 k 和 t 。
    // 请你判断是否存在 两个不同下标 i 和 j，使得 abs(nums[i] - nums[j]) <= t ，同时又满足 abs(i - j) <= k 。
    // 如果存在则返回 true，不存在返回 false。
    // 下标差在 k 之内，大小差在 t 之内
    // 提示：
    // 0 <= nums.length <= 2 * 104
    // -231 <= nums[i] <= 231 - 1
    // 0 <= k <= 104
    // 0 <= t <= 231 - 1

    // 方法一：桶排序-时间复杂度：O(n)，空间复杂度：O(min(n,k))
    // 难点就在于确定桶的取值范围
    // 按照元素的大小进行分桶，维护一个滑动窗口内的元素对应的元素。（即桶的个数维持在k个）
    // 实现方面，我们将 int 范围内的每一个整数 x 表示为 x=(t+1)×a+b (0≤b≤t) 的形式，这样 x 即归属于编号为 a 的桶。
    // 精髓之一：因为一个桶内至多只会有一个元素（不然的话此时已经返回true了），所以我们使用哈希表实现即可。
    public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        int n = nums.length;
        Map<Long, Long> map = new HashMap<Long, Long>();// 桶，<桶的编号，桶内的数>
        long w = (long) t + 1;
        for (int i = 0; i < n; i++) {
            long id = getID(nums[i], w);
            // 检查同编号桶（同编号桶的数大小一定在t之内）
            if (map.containsKey(id))
                return true;

            // 检查相邻编号的桶（不同编号桶的数大小，在检查是否在t之内）
            if (map.containsKey(id - 1) && Math.abs(nums[i] - map.get(id - 1)) < w)
                return true;

            if (map.containsKey(id + 1) && Math.abs(nums[i] - map.get(id + 1)) < w)
                return true;

            map.put(id, (long) nums[i]);
            if (i >= k)// 从索引k开始后，窗口内元素已满k个，每次移除1个元素（窗口最左边的元素）
                map.remove(getID(nums[i - k], w));

        }
        return false;
    }

    // 计算数 x 归属于哪个编号的桶
    public long getID(long x, long w) {
        // w = t + 1
        // 桶1：t+1,...,2t+1 (t+1)*1 + [0, t]
        // 桶0：0,1,2,....,t (t+1)*0 + [0, t]
        // 桶-1:-1,....,-t-1 (t+1)*-1+ [0, t]
        if (x >= 0)
            return x / w;
        else
            return (x + 1) / w - 1;
    }

    // 方法一：（自己写的，更好理解）桶排序-时间复杂度：O(n)，空间复杂度：O(min(n,k))
    long t;

    public boolean containsNearbyAlmostDuplicate11(int[] nums, int k, int t) {
        this.t = t;
        Map<Long, Long> bucketNum = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];
            long curBucket = getBucketId(num);
            if (bucketNum.containsKey(curBucket))
                return true;

            if (bucketNum.containsKey(curBucket - 1) && num - bucketNum.get(curBucket - 1) <= t)
                return true;

            if (bucketNum.containsKey(curBucket + 1) && bucketNum.get(curBucket + 1) - num <= t)
                return true;

            bucketNum.put(curBucket, (long) num);
            if (i >= k)
                bucketNum.remove(getBucketId(nums[i - k]));
        }
        return false;
    }

    // 更好理解的分桶
    private long getBucketId(int num) {
        if (num < 0)
            return num / (t + 1) - 1;
        else
            return num / (t + 1);

    }

    // 方法二：滑动窗口 + 有序集合 TreeSet（窗口内有重复值时，可能会误删还在窗口内的元素）
    // 时间复杂度：O(nlog(min(n,k)))，空间复杂度：O(min(n,k))
    // 为防止整型 int 溢出，我们既可以使用长整型 long，
    // 也可以对查找区间 [x - t, x + t] 进行限制，使其落在 int 范围内。
    public boolean containsNearbyAlmostDuplicate2(int[] nums, int k, int t) {
        int n = nums.length;
        TreeSet<Long> set = new TreeSet<Long>();
        for (int i = 0; i < n; i++) {
            // 1. 判断一定要在删除元素之前
            // 2. 插入当前元素 nums[i] 之前，先检查有没有元素处在 - t + nums[i] <= t + nums[i] 之间
            // 3. 只调用set.ceiling即可（少调用一次floor，单次查询的时间复杂度为O(k)）
            // 4. 需用包装类接收返回，方便后面判空
            Long ceiling = set.ceiling((long) nums[i] - (long) t);
            if (ceiling != null && ceiling <= (long) nums[i] + (long) t)
                return true;

            set.add((long) nums[i]);

            if (i >= k) // 从索引k开始后，窗口内元素已满k个，每次移除1个元素（窗口最左边的元素）
                set.remove((long) nums[i - k]);
        }
        return false;
    }

    // 方法二：（自己写的）滑动窗口 + 有序哈希 TreeMap
    // 时间复杂度：O(nlog(min(n,k)))，空间复杂度：O(min(n,k))
    public boolean containsNearbyAlmostDuplicate22(int[] nums, int k, int t) {
        if (k == 0)
            return false;

        TreeMap<Integer, Integer> map = new TreeMap<>();
        for (int i = 0; i < nums.length; i++) {
            int num = nums[i];

            // 这里使用两次查找，时间复杂度确实翻倍了...可以优化为只使用一次，稍麻烦
            Integer ceiling = map.ceilingKey(num);
            Integer floor = map.floorKey(num);
            if (ceiling != null && (long) ceiling - num <= t)
                return true;

            if (floor != null && (long) num - floor <= t)
                return true;

            if (i >= k) {
                int key = nums[i - k];
                int value = map.get(key);
                if (value == 1)// 值为0的话，必须删掉键值，因为查找范围内是否有值，是根据键值来的
                    map.remove(key);
                else
                    map.put(key, value - 1);
            }
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        return false;
    }

    // 剑指Offer II 060.出现频率最高..

    // 剑指Offer II 074.合并区间（主站56. hot100有）
    // 以数组 intervals 表示若干个区间的集合，其中单个区间为 intervals[i] = [starti, endi] 。
    // 请你合并所有重叠的区间，并返回一个不重叠的区间数组，该数组需恰好覆盖输入中的所有区间。

    // 方法一：排序-时间复杂度：O(nlogn)，空间复杂度：O(logn)
    public int[][] merge(int[][] intervals) {
        if (intervals.length == 0)
            return new int[0][2];

        Comparator<int[]> c = (interval1, interval2) -> interval1[0] - interval2[0];// 以start升序排列

        Arrays.sort(intervals, c);
        // 用数组 merged 存储最终的答案
        Deque<int[]> merged = new LinkedList<int[]>();
        for (int i = 0; i < intervals.length; ++i) {
            int L = intervals[i][0], R = intervals[i][1];
            // merged数组中还无数组，直接加入（初始情况）；或者merged末尾二元组的R小于L（不能合并），加入
            if (merged.size() == 0 || merged.peek()[1] < L)
                merged.push(new int[] { L, R });
            else {// merged数组末尾大于L（能合并），更新merged末尾二元组的R
                int[] lastInterval = merged.pop();
                merged.push(new int[] { lastInterval[0], Math.max(lastInterval[1], R) });
            }
        }
        return merged.toArray(new int[merged.size()][]);
    }

    // 剑指Offer II 075.数组相对排序

    // 剑指Offer II 076.数组中的第 k 大的数字（主站215. hot100有）

    // 剑指Offer II 077.链表排序

}

class Solution {
    Map<String, Integer> strId = new HashMap<>();
    List<List<Integer>> graph = new ArrayList<>();
    boolean[] visited;

    public int numSimilarGroups(String[] strs) {
        int index = 0;
        for (String str : strs) {
            strId.put(str, index++);
            graph.add(new ArrayList<>());
        }

        for (String str1 : strs) {
            for (String str2 : strs) {
                if (check(str1, str2)) {
                    graph.get(strId.get(str1)).add(strId.get(str2));
                }
            }
        }

        visited = new boolean[index];
        int res = 0;
        for (String str : strs) {
            int id = strId.get(str);
            if (!visited[id]) {
                res++;
                dfs(strId.get(str));
            }
        }
        return res;
    }

    private void dfs(int id) {
        visited[id] = true;
        for (int next : graph.get(id)) {
            if (!visited[next])
                dfs(next);
        }
    }

    private boolean check(String str1, String str2) {
        if (str1.length() != str2.length())
            return false;
        List<Integer> list = new ArrayList<>();
        for (int i = 0; i < str1.length(); i++) {
            if (str1.charAt(i) != str2.charAt(i))
                list.add(i);

            if (list.size() > 2)
                return false;
        }
        if (list.size() != 2)
            return false;
        int index1 = list.get(0);
        int index2 = list.get(1);
        if (str1.charAt(index1) == str2.charAt(index2) && str1.charAt(index2) == str2.charAt(index1))
            return true;
        else
            return false;
    }
}

public class JianzhiOfferII {

    public static void main(String[] args) {
        System.out.println(new Solution().numSimilarGroups(new String[] { "tars", "rats", "arts", "star" }));
    }
}
